mirror of https://github.com/apache/cassandra
Improve checksumming and compression in protocol V5
This reverts the changes made to the native protocol V5 wire format in CASSANDRA-13304 and subsequent follow up JIRAs (CASSANDRA-15556 & CASSANDRA-14716). The framing format has been re-implemented based on the internode messaging format developed in CASSANDRA-15066. OPTIONS and STARTUP messages are unframed (i.e. identical to the V4 format). After sending its response to a STARTUP message, the server modifies the pipeline and all further messages are sent (and should be expected by the client to be received) in the format determined by the protocol version of the STARTUP message. Patch by Sam Tunnicliffe; reviewed by Alex Petrov and Caleb Rackliffe for CASSANDRA-15299
This commit is contained in:
parent
22abff779d
commit
a7c4ba9eee
|
|
@ -1,4 +1,5 @@
|
|||
4.0-beta4
|
||||
* Improve checksumming and compression in protocol V5 (CASSANDRA-15299)
|
||||
* Optimised repair streaming improvements (CASSANDRA-16274)
|
||||
* Update jctools dependency to 3.1.0 (CASSANDRA-16255)
|
||||
* 'SSLEngine closed already' exception on failed outbound connection (CASSANDRA-16277)
|
||||
|
|
|
|||
|
|
@ -475,6 +475,8 @@ class Shell(cmd.Cmd):
|
|||
kwargs = {}
|
||||
if protocol_version is not None:
|
||||
kwargs['protocol_version'] = protocol_version
|
||||
else:
|
||||
kwargs['protocol_version'] = 4
|
||||
self.conn = Cluster(contact_points=(self.hostname,), port=self.port, cql_version=cqlver,
|
||||
auth_provider=self.auth_provider,
|
||||
ssl_options=sslhandling.ssl_settings(hostname, CONFIG_FILE) if ssl else None,
|
||||
|
|
|
|||
|
|
@ -644,7 +644,7 @@
|
|||
</dependency>
|
||||
<dependency groupId="com.google.code.findbugs" artifactId="jsr305" version="2.0.2" />
|
||||
<dependency groupId="com.clearspring.analytics" artifactId="stream" version="2.5.2" />
|
||||
<dependency groupId="com.datastax.cassandra" artifactId="cassandra-driver-core" version="3.9.0" classifier="shaded">
|
||||
<dependency groupId="com.datastax.cassandra" artifactId="cassandra-driver-core" version="3.10.0" classifier="shaded">
|
||||
<exclusion groupId="io.netty" artifactId="netty-buffer"/>
|
||||
<exclusion groupId="io.netty" artifactId="netty-codec"/>
|
||||
<exclusion groupId="io.netty" artifactId="netty-handler"/>
|
||||
|
|
@ -939,6 +939,7 @@
|
|||
<include name="asm-*" />
|
||||
</fileset>
|
||||
</delete>
|
||||
|
||||
</target>
|
||||
|
||||
<target name="maven-ant-tasks-retrieve-test" depends="maven-ant-tasks-init">
|
||||
|
|
|
|||
|
|
@ -699,10 +699,6 @@ native_transport_port: 9042
|
|||
# you may want to adjust max_value_size_in_mb accordingly. This should be positive and less than 2048.
|
||||
# native_transport_max_frame_size_in_mb: 256
|
||||
|
||||
# If checksumming is enabled as a protocol option, denotes the size of the chunks into which frame
|
||||
# are bodies will be broken and checksummed.
|
||||
# native_transport_frame_block_size_in_kb: 32
|
||||
|
||||
# The maximum number of concurrent client connections.
|
||||
# The default is -1, which means unlimited.
|
||||
# native_transport_max_concurrent_connections: -1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
Framing Format Changes For Native Protocol V5
|
||||
---------------------------------------------
|
||||
|
||||
The framing format for V5 is based on the internode messaging system as it was redesigned for 4.0 in https://issues.apache.org/jira/browse/CASSANDRA-15066[CASSANDRA-15066]. In prior versions, the relationship between `Message` and `Frame` is 1:1, each CQL message being contained in exactly one frame and every frame containing only a single message. The changes introduced here enable frames to contain multiple small messages and for oversized messages to be broken into multiple frames.
|
||||
|
||||
The introduction of this new format, brings an unfortunate naming clash between frames from the previous format, with a 1:1 relationship to messages, and the new outer frames. Taking inspiration from SMTP (RFC-5321), what was previously referred to as a `Frame` is better described as an `Envelope` which comprises a `CQL message` along with some attendant metadata.
|
||||
|
||||
|
||||
|
||||
New Frame Format
|
||||
----------------
|
||||
|
||||
In general, a `frame` comprises a `header`, `payload` and `trailer`; this proposal introduces two specific `frame` formats, `compressed` and `uncompressed`. In both cases, the `payload` is a stream of `CQL Envelopes`, each containing a single `CQL Message`. In effect, the new framing format is a simple wrapper around the previous protocol.
|
||||
|
||||
In all cases, a `frame` may or may not be `self contained`. If `self contained`, the `payload` includes
|
||||
one or more complete `CQL envelopes` and can be fully processed immediately. Otherwise, the `payload` contains some part of a large `CQL envelope`, which has been split into its own sequence of `outer frames`. These are expected to be transmitted/received in order, so a processor can accumulate them as they arrive and process them once all have been received.
|
||||
|
||||
The header contains length information for the `payload`, whether or not the frame is `self contained` and a `CRC` to protect the integrity of the `header` itself. There are slight variations in the `header` format between the compressed and uncompressed variants.
|
||||
|
||||
The payload is opaque as far as the framing format is concerned, modulo the `self contained` variation.
|
||||
|
||||
The `trailer` contains a `CRC` to protect the integrity of the `payload`. As the `payload` includes the `CQL envelopes` including both the message and the attached metadata, such as the `stream ID`, `flags` and `opcode`, this adds a level of protection that previously wasn't available (for example from the implementation in https://issues.apache.org/jira/browse/CASSANDRA-13304[CASSANDRA-13304]).
|
||||
|
||||
|
||||
Uncompressed Format
|
||||
-------------------
|
||||
|
||||
The uncompressed variant uses a 6 byte `header` containing `payload length`, `self contained flag` and `CRC24` for the `header`. The max size for the payload is `128KiB`, and is followed by its `CRC32`.
|
||||
|
||||
....
|
||||
1. Payload length (17 bits)
|
||||
2. isSelfContained flag (1 bit)
|
||||
3. Header padding (6 bits)
|
||||
4. CRC24 of the header (24 bits)
|
||||
5. Payload (up to 2 ^ 17 - 1 bits)
|
||||
6. Payload CRC32 (32 bits)
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Payload Length |C| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
CRC24 of Header | |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
|
||||
| |
|
||||
+ +
|
||||
| Payload |
|
||||
+ +
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| CRC32 of Payload |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
....
|
||||
|
||||
LZ4 Compressed Format
|
||||
---------------------
|
||||
|
||||
The variant with `LZ4` compression uses an `8 byte header`, containing both the compressed and uncompressed lengths of the `payload`, the `self contained flag` and a `CRC24` for the `header`. As with uncompressed frames, the max payload size is `128KiB` and is followed by a `CRC32` trailer. This is the `CRC` of the *compressed* `payload`.
|
||||
|
||||
....
|
||||
1. Compressed length (17 bits)
|
||||
2. Uncompressed length (17 bits)
|
||||
3. {@code isSelfContained} flag (1 bit)
|
||||
4. Header padding (5 bits)
|
||||
5. CRC24 of Header contents (24 bits)
|
||||
6. Compressed Payload (up to 2 ^ 17 - 1 bits)
|
||||
7. CRC32 of Compressed Payload (32 bits)
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Compressed Length | Uncompressed Length
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|C| | CRC24 of Header |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
+ +
|
||||
| Compressed Payload |
|
||||
+ +
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| CRC32 of Compressed Payload |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
....
|
||||
|
||||
|
||||
Protocol Changes
|
||||
----------------
|
||||
Other than the enclosure of `CQL envelopes` in `frames`, the changes required to the protocol itself are minimal. All message exchanges follow the same `REQUEST/RESPONSE` specifications as before and no changes are required to either the `CQL envelope (frame)` or `CQL message` formats. `CQL envelope` headers contain some information which is now redundant and currently ignored. The `CQL envelope` header contains the protocol version, which was always to some extent redundant as the version is set and enforced at the connection level. It was also previously possible to enable compression at the individual `CQL envelope` level. This is no longer an option, the outer framing format being responsible for compression, which is set for the lifetime of a connection and applies to all messages transmitted throughout it. To that end, the `compression` flag on the `CQL envelope` header is ignored in V5.
|
||||
|
||||
** TODO - note on conditional compression
|
||||
|
||||
Protocol Negotiation
|
||||
--------------------
|
||||
|
||||
In order to support both V5 and earlier formats, the V5 `outer framing` format is not applied to message exchanges *before* a `STARTUP` exchange is completed. This means that the initial `STARTUP` message and any `OPTIONS` messages which precede it are expected *without* the `outer framing`. Likewise, the responses returned by the server (`SUPPORTED` for `OPTIONS` and either `READY` or `AUTHENTICATE` for `STARTUP`) are transmitted without the `outer framing`.
|
||||
|
||||
After sending the response to a `STARTUP` (`READY` or `AUTHENTICATE`), the server will begin encoding and decoding further transmissions according to the protocol version of that `STARTUP` message. Compression of the `outer frames` is dictated by the `COMPRESSION` option sent in the `STARTUP` message. Only `LZ4` compression is currently supported for V5.
|
||||
|
||||
Note: `OPTIONS` requests may be sent by the client at any time in the connection lifecycle, both before and after the `STARTUP` exchange. As mentioned, those transmitted _before_ `STARTUP`, as well as the `SUPPORTED` responses the server returns are *not* enclosed in `outer frames`. Any `OPTIONS/SUPPORTED` exchanges _after_ the `STARTUP` exchange *are* formatted according to the protocol version. So in V5, these *will* be enclosed in `outer frames`.
|
||||
|
||||
Binary file not shown.
|
|
@ -188,13 +188,13 @@ public class Config
|
|||
public volatile long native_transport_max_concurrent_connections_per_ip = -1L;
|
||||
public boolean native_transport_flush_in_batches_legacy = false;
|
||||
public volatile boolean native_transport_allow_older_protocols = true;
|
||||
public int native_transport_frame_block_size_in_kb = 32;
|
||||
public volatile long native_transport_max_concurrent_requests_in_bytes_per_ip = -1L;
|
||||
public volatile long native_transport_max_concurrent_requests_in_bytes = -1L;
|
||||
public int native_transport_receive_queue_capacity_in_bytes = 1 << 20; // 1MiB
|
||||
|
||||
@Deprecated
|
||||
public Integer native_transport_max_negotiable_protocol_version = null;
|
||||
|
||||
|
||||
/**
|
||||
* Max size of values in SSTables, in MegaBytes.
|
||||
* Default is the same as the native protocol frame limit: 256Mb.
|
||||
|
|
|
|||
|
|
@ -514,9 +514,6 @@ public class DatabaseDescriptor
|
|||
checkValidForByteConversion(conf.batch_size_warn_threshold_in_kb,
|
||||
"batch_size_warn_threshold_in_kb", ByteUnit.KIBI_BYTES);
|
||||
|
||||
checkValidForByteConversion(conf.native_transport_frame_block_size_in_kb,
|
||||
"native_transport_frame_block_size_in_kb", ByteUnit.KIBI_BYTES);
|
||||
|
||||
if (conf.native_transport_max_negotiable_protocol_version != null)
|
||||
logger.warn("The configuration option native_transport_max_negotiable_protocol_version has been deprecated " +
|
||||
"and should be removed from cassandra.yaml as it has no longer has any effect.");
|
||||
|
|
@ -2192,11 +2189,6 @@ public class DatabaseDescriptor
|
|||
conf.native_transport_allow_older_protocols = isEnabled;
|
||||
}
|
||||
|
||||
public static int getNativeTransportFrameBlockSize()
|
||||
{
|
||||
return (int) ByteUnit.KIBI_BYTES.toBytes(conf.native_transport_frame_block_size_in_kb);
|
||||
}
|
||||
|
||||
public static double getCommitLogSyncGroupWindow()
|
||||
{
|
||||
return conf.commitlog_sync_group_window_in_ms;
|
||||
|
|
@ -2207,6 +2199,16 @@ public class DatabaseDescriptor
|
|||
conf.commitlog_sync_group_window_in_ms = windowMillis;
|
||||
}
|
||||
|
||||
public static int getNativeTransportReceiveQueueCapacityInBytes()
|
||||
{
|
||||
return conf.native_transport_receive_queue_capacity_in_bytes;
|
||||
}
|
||||
|
||||
public static void setNativeTransportReceiveQueueCapacityInBytes(int queueSize)
|
||||
{
|
||||
conf.native_transport_receive_queue_capacity_in_bytes = queueSize;
|
||||
}
|
||||
|
||||
public static long getNativeTransportMaxConcurrentRequestsInBytesPerIp()
|
||||
{
|
||||
return conf.native_transport_max_concurrent_requests_in_bytes_per_ip;
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ import com.codahale.metrics.Histogram;
|
|||
/**
|
||||
* Metrics to track the size of incoming and outgoing bytes at Cassandra server.
|
||||
*/
|
||||
public class ClientRequestSizeMetrics
|
||||
public class ClientMessageSizeMetrics
|
||||
{
|
||||
private static final String TYPE = "ClientRequestSize";
|
||||
public static final Counter totalBytesRead = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "IncomingBytes", null));
|
||||
public static final Counter totalBytesWritten = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "OutgoingBytes", null));
|
||||
public static final Histogram bytesReceivedPerFrame = Metrics.histogram(DefaultNameFactory.createMetricName(TYPE, "BytesRecievedPerFrame", null), true);
|
||||
public static final Histogram bytesTransmittedPerFrame = Metrics.histogram(DefaultNameFactory.createMetricName(TYPE, "BytesTransmittedPerFrame", null), true);
|
||||
private static final String TYPE = "ClientMessageSize";
|
||||
public static final Counter bytesReceived = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "BytesReceived", null));
|
||||
public static final Counter bytesSent = Metrics.counter(DefaultNameFactory.createMetricName(TYPE, "BytesSent", null));
|
||||
public static final Histogram bytesReceivedPerRequest = Metrics.histogram(DefaultNameFactory.createMetricName(TYPE, "BytesReceivedPerRequest", null), true);
|
||||
public static final Histogram bytesSentPerResponse = Metrics.histogram(DefaultNameFactory.createMetricName(TYPE, "BytesSentPerResponse", null), true);
|
||||
}
|
||||
|
|
@ -25,9 +25,7 @@ import com.codahale.metrics.Gauge;
|
|||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.Reservoir;
|
||||
import org.apache.cassandra.transport.ClientStat;
|
||||
import org.apache.cassandra.transport.ConnectedClient;
|
||||
import org.apache.cassandra.transport.Server;
|
||||
import org.apache.cassandra.transport.*;
|
||||
|
||||
import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
|
||||
|
||||
|
|
@ -90,9 +88,9 @@ public final class ClientMetrics
|
|||
registerGauge("ConnectedNativeClientsByUser", "connectedNativeClientsByUser", this::countConnectedClientsByUser);
|
||||
registerGauge("Connections", "connections", this::connectedClients);
|
||||
registerGauge("ClientsByProtocolVersion", "clientsByProtocolVersion", this::recentClientStats);
|
||||
registerGauge("RequestsSize", Server.EndpointPayloadTracker::getCurrentGlobalUsage);
|
||||
registerGauge("RequestsSize", ClientResourceLimits::getCurrentGlobalUsage);
|
||||
|
||||
Reservoir ipUsageReservoir = Server.EndpointPayloadTracker.ipUsageReservoir();
|
||||
Reservoir ipUsageReservoir = ClientResourceLimits.ipUsageReservoir();
|
||||
Metrics.register(factory.createMetricName("RequestsSizeByIpDistribution"),
|
||||
new Histogram(ipUsageReservoir)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,829 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.cassandra.net;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
|
||||
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.EventLoop;
|
||||
import org.apache.cassandra.net.FrameDecoder.CorruptFrame;
|
||||
import org.apache.cassandra.net.FrameDecoder.Frame;
|
||||
import org.apache.cassandra.net.FrameDecoder.FrameProcessor;
|
||||
import org.apache.cassandra.net.FrameDecoder.IntactFrame;
|
||||
import org.apache.cassandra.net.Message.Header;
|
||||
import org.apache.cassandra.net.ResourceLimits.Limit;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
import static org.apache.cassandra.net.Crc.InvalidCrc;
|
||||
import static org.apache.cassandra.utils.MonotonicClock.approxTime;
|
||||
|
||||
/**
|
||||
* Core logic for handling inbound message deserialization and execution (in tandem with {@link FrameDecoder}).
|
||||
*
|
||||
* Handles small and large messages, corruption, flow control, dispatch of message processing to a suitable
|
||||
* consumer.
|
||||
*
|
||||
* # Interaction with {@link FrameDecoder}
|
||||
*
|
||||
* An {@link AbstractMessageHandler} implementation sits on top of a {@link FrameDecoder} in the Netty pipeline,
|
||||
* and is tightly coupled with it.
|
||||
*
|
||||
* {@link FrameDecoder} decodes inbound frames and relies on a supplied {@link FrameProcessor} to act on them.
|
||||
* {@link AbstractMessageHandler} provides two implementations of that interface:
|
||||
* - {@link #process(Frame)} is the default, primary processor, and is expected to be implemented by subclasses
|
||||
* - {@link UpToOneMessageFrameProcessor}, supplied to the decoder when the handler is reactivated after being
|
||||
* put in waiting mode due to lack of acquirable reserve memory capacity permits
|
||||
*
|
||||
* Return value of {@link FrameProcessor#process(Frame)} determines whether the decoder should keep processing
|
||||
* frames (if {@code true} is returned) or stop until explicitly reactivated (if {@code false} is). To reactivate
|
||||
* the decoder (once notified of available resource permits), {@link FrameDecoder#reactivate()} is invoked.
|
||||
*
|
||||
* # Frames
|
||||
*
|
||||
* {@link AbstractMessageHandler} operates on frames of messages, and there are several kinds of them:
|
||||
* 1. {@link IntactFrame} that are contained. As names suggest, these contain one or multiple fully contained
|
||||
* messages believed to be uncorrupted. Guaranteed to not contain an part of an incomplete message.
|
||||
* See {@link #processFrameOfContainedMessages(ShareableBytes, Limit, Limit)}.
|
||||
* 2. {@link IntactFrame} that are NOT contained. These are uncorrupted parts of a large message split over multiple
|
||||
* parts due to their size. Can represent first or subsequent frame of a large message.
|
||||
* See {@link #processFirstFrameOfLargeMessage(IntactFrame, Limit, Limit)} and
|
||||
* {@link #processSubsequentFrameOfLargeMessage(Frame)}.
|
||||
* 3. {@link CorruptFrame} with corrupt header. These are unrecoverable, and force a connection to be dropped.
|
||||
* 4. {@link CorruptFrame} with a valid header, but corrupt payload. These can be either contained or uncontained.
|
||||
* - contained frames with corrupt payload can be gracefully dropped without dropping the connection
|
||||
* - uncontained frames with corrupt payload can be gracefully dropped unless they represent the first
|
||||
* frame of a new large message, as in that case we don't know how many bytes to skip
|
||||
* See {@link #processCorruptFrame(CorruptFrame)}.
|
||||
*
|
||||
* Fundamental frame invariants:
|
||||
* 1. A contained frame can only have fully-encapsulated messages - 1 to n, that don't cross frame boundaries
|
||||
* 2. An uncontained frame can hold a part of one message only. It can NOT, say, contain end of one large message
|
||||
* and a beginning of another one. All the bytes in an uncontained frame always belong to a single message.
|
||||
*
|
||||
* # Small vs large messages
|
||||
*
|
||||
* A single handler is equipped to process both small and large messages, potentially interleaved, but the logic
|
||||
* differs depending on size. Small messages are deserialized in place, and then handed off to an appropriate
|
||||
* thread pool for processing. Large messages accumulate frames until completion of a message, then hand off
|
||||
* the untouched frames to the correct thread pool for the verb to be deserialized there and immediately processed.
|
||||
*
|
||||
* See {@link LargeMessage} and subclasses for concrete {@link AbstractMessageHandler} implementations for details
|
||||
* of the large-message accumulating state-machine, and {@link ProcessMessage} and its inheritors for the differences
|
||||
*in execution.
|
||||
*
|
||||
* # Flow control (backpressure)
|
||||
*
|
||||
* To prevent message producers from overwhelming and bringing nodes down with more inbound messages that
|
||||
* can be processed in a timely manner, {@link AbstractMessageHandler} provides support for implementations to
|
||||
* provide their own flow control policy.
|
||||
*
|
||||
* Before we attempt to process a message fully, we first infer its size from the stream. This inference is
|
||||
* delegated to implementations as the encoding of the message size is protocol specific. Having assertained
|
||||
* the size of the incoming message, we then attempt to acquire the corresponding number of memory permits.
|
||||
* If we succeed, then we move on actually process the message. If we fail, the frame decoder deactivates
|
||||
* until sufficient permits are released for the message to be processed and the handler is activated again.
|
||||
* Permits are released back once the message has been fully processed - the definition of which is again
|
||||
* delegated to the concrete implementations.
|
||||
*
|
||||
* Every connection has an exclusive number of permits allocated to it. In addition to it, there is a per-endpoint
|
||||
* reserve capacity and a global reserve capacity {@link Limit}, shared between all connections from the same host
|
||||
* and all connections, respectively. So long as long as the handler stays within its exclusive limit, it doesn't
|
||||
* need to tap into reserve capacity.
|
||||
*
|
||||
* If tapping into reserve capacity is necessary, but the handler fails to acquire capacity from either
|
||||
* endpoint of global reserve (and it needs to acquire from both), the handler and its frame decoder become
|
||||
* inactive and register with a {@link WaitQueue} of the appropriate type, depending on which of the reserves
|
||||
* couldn't be tapped into. Once enough messages have finished processing and had their permits released back
|
||||
* to the reserves, {@link WaitQueue} will reactivate the sleeping handlers and they'll resume processing frames.
|
||||
*
|
||||
* The reason we 'split' reserve capacity into two limits - endpoing and global - is to guarantee liveness, and
|
||||
* prevent single endpoint's connections from taking over the whole reserve, starving other connections.
|
||||
*
|
||||
* One permit per byte of serialized message gets acquired. When inflated on-heap, each message will occupy more
|
||||
* than that, necessarily, but despite wide variance, it's a good enough proxy that correlates with on-heap footprint.
|
||||
*/
|
||||
public abstract class AbstractMessageHandler extends ChannelInboundHandlerAdapter implements FrameProcessor
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(AbstractMessageHandler.class);
|
||||
private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1L, TimeUnit.SECONDS);
|
||||
|
||||
protected final FrameDecoder decoder;
|
||||
|
||||
protected final Channel channel;
|
||||
|
||||
protected final int largeThreshold;
|
||||
protected LargeMessage<?> largeMessage;
|
||||
|
||||
protected final long queueCapacity;
|
||||
volatile long queueSize = 0L;
|
||||
private static final AtomicLongFieldUpdater<AbstractMessageHandler> queueSizeUpdater =
|
||||
AtomicLongFieldUpdater.newUpdater(AbstractMessageHandler.class, "queueSize");
|
||||
|
||||
protected final Limit endpointReserveCapacity;
|
||||
protected final WaitQueue endpointWaitQueue;
|
||||
|
||||
protected final Limit globalReserveCapacity;
|
||||
protected final WaitQueue globalWaitQueue;
|
||||
|
||||
protected final OnHandlerClosed onClosed;
|
||||
|
||||
// wait queue handle, non-null if we overrun endpoint or global capacity and request to be resumed once it's released
|
||||
private WaitQueue.Ticket ticket = null;
|
||||
|
||||
protected long corruptFramesRecovered, corruptFramesUnrecovered;
|
||||
protected long receivedCount, receivedBytes;
|
||||
protected long throttledCount, throttledNanos;
|
||||
|
||||
private boolean isClosed;
|
||||
|
||||
public AbstractMessageHandler(FrameDecoder decoder,
|
||||
|
||||
Channel channel,
|
||||
int largeThreshold,
|
||||
|
||||
long queueCapacity,
|
||||
Limit endpointReserveCapacity,
|
||||
Limit globalReserveCapacity,
|
||||
WaitQueue endpointWaitQueue,
|
||||
WaitQueue globalWaitQueue,
|
||||
|
||||
OnHandlerClosed onClosed)
|
||||
{
|
||||
this.decoder = decoder;
|
||||
|
||||
this.channel = channel;
|
||||
this.largeThreshold = largeThreshold;
|
||||
|
||||
this.queueCapacity = queueCapacity;
|
||||
this.endpointReserveCapacity = endpointReserveCapacity;
|
||||
this.endpointWaitQueue = endpointWaitQueue;
|
||||
this.globalReserveCapacity = globalReserveCapacity;
|
||||
this.globalWaitQueue = globalWaitQueue;
|
||||
|
||||
this.onClosed = onClosed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg)
|
||||
{
|
||||
/*
|
||||
* InboundMessageHandler works in tandem with FrameDecoder to implement flow control
|
||||
* and work stashing optimally. We rely on FrameDecoder to invoke the provided
|
||||
* FrameProcessor rather than on the pipeline and invocations of channelRead().
|
||||
* process(Frame) is the primary entry point for this class.
|
||||
*/
|
||||
throw new IllegalStateException("InboundMessageHandler doesn't expect channelRead() to be invoked");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx)
|
||||
{
|
||||
decoder.activate(this); // the frame decoder starts inactive until explicitly activated by the added inbound message handler
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(Frame frame) throws IOException
|
||||
{
|
||||
if (frame instanceof IntactFrame)
|
||||
return processIntactFrame((IntactFrame) frame, endpointReserveCapacity, globalReserveCapacity);
|
||||
|
||||
processCorruptFrame((CorruptFrame) frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean processIntactFrame(IntactFrame frame, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
if (frame.isSelfContained)
|
||||
return processFrameOfContainedMessages(frame.contents, endpointReserve, globalReserve);
|
||||
else if (null == largeMessage)
|
||||
return processFirstFrameOfLargeMessage(frame, endpointReserve, globalReserve);
|
||||
else
|
||||
return processSubsequentFrameOfLargeMessage(frame);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle contained messages (not crossing boundaries of the frame) - both small and large, for the inbound
|
||||
* definition of large (breaching the size threshold for what we are willing to process on event-loop vs.
|
||||
* off event-loop).
|
||||
*/
|
||||
private boolean processFrameOfContainedMessages(ShareableBytes bytes, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
while (bytes.hasRemaining())
|
||||
if (!processOneContainedMessage(bytes, endpointReserve, globalReserve))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract boolean processOneContainedMessage(ShareableBytes bytes, Limit endpointReserve, Limit globalReserve) throws IOException;
|
||||
|
||||
|
||||
/*
|
||||
* Handling of multi-frame large messages
|
||||
*/
|
||||
|
||||
protected abstract boolean processFirstFrameOfLargeMessage(IntactFrame frame, Limit endpointReserve, Limit globalReserve) throws IOException;
|
||||
|
||||
protected boolean processSubsequentFrameOfLargeMessage(Frame frame)
|
||||
{
|
||||
receivedBytes += frame.frameSize;
|
||||
if (largeMessage.supply(frame))
|
||||
{
|
||||
receivedCount++;
|
||||
largeMessage = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* We can handle some corrupt frames gracefully without dropping the connection and losing all the
|
||||
* queued up messages, but not others.
|
||||
*
|
||||
* Corrupt frames that *ARE NOT* safe to skip gracefully and require the connection to be dropped:
|
||||
* - any frame with corrupt header (!frame.isRecoverable())
|
||||
* - first corrupt-payload frame of a large message (impossible to infer message size, and without it
|
||||
* impossible to skip the message safely
|
||||
*
|
||||
* Corrupt frames that *ARE* safe to skip gracefully, without reconnecting:
|
||||
* - any self-contained frame with a corrupt payload (but not header): we lose all the messages in the
|
||||
* frame, but that has no effect on subsequent ones
|
||||
* - any non-first payload-corrupt frame of a large message: we know the size of the large message in
|
||||
* flight, so we just skip frames until we've seen all its bytes; we only lose the large message
|
||||
*/
|
||||
protected abstract void processCorruptFrame(CorruptFrame frame) throws InvalidCrc;
|
||||
|
||||
private void onEndpointReserveCapacityRegained(Limit endpointReserve, long elapsedNanos)
|
||||
{
|
||||
onReserveCapacityRegained(endpointReserve, globalReserveCapacity, elapsedNanos);
|
||||
}
|
||||
|
||||
private void onGlobalReserveCapacityRegained(Limit globalReserve, long elapsedNanos)
|
||||
{
|
||||
onReserveCapacityRegained(endpointReserveCapacity, globalReserve, elapsedNanos);
|
||||
}
|
||||
|
||||
private void onReserveCapacityRegained(Limit endpointReserve, Limit globalReserve, long elapsedNanos)
|
||||
{
|
||||
if (isClosed)
|
||||
return;
|
||||
|
||||
assert channel.eventLoop().inEventLoop();
|
||||
|
||||
ticket = null;
|
||||
throttledNanos += elapsedNanos;
|
||||
|
||||
try
|
||||
{
|
||||
/*
|
||||
* Process up to one message using supplied overriden reserves - one of them pre-allocated,
|
||||
* and guaranteed to be enough for one message - then, if no obstacles enountered, reactivate
|
||||
* the frame decoder using normal reserve capacities.
|
||||
*/
|
||||
if (processUpToOneMessage(endpointReserve, globalReserve))
|
||||
decoder.reactivate();
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
fatalExceptionCaught(t);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void fatalExceptionCaught(Throwable t);
|
||||
|
||||
// return true if the handler should be reactivated - if no new hurdles were encountered,
|
||||
// like running out of the other kind of reserve capacity
|
||||
private boolean processUpToOneMessage(Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
UpToOneMessageFrameProcessor processor = new UpToOneMessageFrameProcessor(endpointReserve, globalReserve);
|
||||
decoder.processBacklog(processor);
|
||||
return processor.isActive;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process at most one message. Won't always be an entire one (if the message in the head of line
|
||||
* is a large one, and there aren't sufficient frames to decode it entirely), but will never be more than one.
|
||||
*/
|
||||
private class UpToOneMessageFrameProcessor implements FrameProcessor
|
||||
{
|
||||
private final Limit endpointReserve;
|
||||
private final Limit globalReserve;
|
||||
|
||||
boolean isActive = true;
|
||||
boolean firstFrame = true;
|
||||
|
||||
private UpToOneMessageFrameProcessor(Limit endpointReserve, Limit globalReserve)
|
||||
{
|
||||
this.endpointReserve = endpointReserve;
|
||||
this.globalReserve = globalReserve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(Frame frame) throws IOException
|
||||
{
|
||||
if (firstFrame)
|
||||
{
|
||||
if (!(frame instanceof IntactFrame))
|
||||
throw new IllegalStateException("First backlog frame must be intact");
|
||||
firstFrame = false;
|
||||
return processFirstFrame((IntactFrame) frame);
|
||||
}
|
||||
|
||||
return processSubsequentFrame(frame);
|
||||
}
|
||||
|
||||
private boolean processFirstFrame(IntactFrame frame) throws IOException
|
||||
{
|
||||
if (frame.isSelfContained)
|
||||
{
|
||||
isActive = processOneContainedMessage(frame.contents, endpointReserve, globalReserve);
|
||||
return false; // stop after one message
|
||||
}
|
||||
else
|
||||
{
|
||||
isActive = processFirstFrameOfLargeMessage(frame, endpointReserve, globalReserve);
|
||||
return isActive; // continue unless fallen behind coprocessor or ran out of reserve capacity again
|
||||
}
|
||||
}
|
||||
|
||||
private boolean processSubsequentFrame(Frame frame) throws IOException
|
||||
{
|
||||
if (frame instanceof IntactFrame)
|
||||
processSubsequentFrameOfLargeMessage(frame);
|
||||
else
|
||||
processCorruptFrame((CorruptFrame) frame);
|
||||
|
||||
return largeMessage != null; // continue until done with the large message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to acquire permits for the inbound message. In case of failure, register with the right wait queue to be
|
||||
* reactivated once permit capacity is regained.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
protected boolean acquireCapacity(Limit endpointReserve, Limit globalReserve, int bytes, long currentTimeNanos, long expiresAtNanos)
|
||||
{
|
||||
ResourceLimits.Outcome outcome = acquireCapacity(endpointReserve, globalReserve, bytes);
|
||||
|
||||
if (outcome == ResourceLimits.Outcome.INSUFFICIENT_ENDPOINT)
|
||||
ticket = endpointWaitQueue.register(this, bytes, currentTimeNanos, expiresAtNanos);
|
||||
else if (outcome == ResourceLimits.Outcome.INSUFFICIENT_GLOBAL)
|
||||
ticket = globalWaitQueue.register(this, bytes, currentTimeNanos, expiresAtNanos);
|
||||
|
||||
if (outcome != ResourceLimits.Outcome.SUCCESS)
|
||||
throttledCount++;
|
||||
|
||||
return outcome == ResourceLimits.Outcome.SUCCESS;
|
||||
}
|
||||
|
||||
protected ResourceLimits.Outcome acquireCapacity(Limit endpointReserve, Limit globalReserve, int bytes)
|
||||
{
|
||||
long currentQueueSize = queueSize;
|
||||
|
||||
/*
|
||||
* acquireCapacity() is only ever called on the event loop, and as such queueSize is only ever increased
|
||||
* on the event loop. If there is enough capacity, we can safely addAndGet() and immediately return.
|
||||
*/
|
||||
if (currentQueueSize + bytes <= queueCapacity)
|
||||
{
|
||||
queueSizeUpdater.addAndGet(this, bytes);
|
||||
return ResourceLimits.Outcome.SUCCESS;
|
||||
}
|
||||
|
||||
// we know we don't have enough local queue capacity for the entire message, so we need to borrow some from reserve capacity
|
||||
long allocatedExcess = min(currentQueueSize + bytes - queueCapacity, bytes);
|
||||
|
||||
if (!globalReserve.tryAllocate(allocatedExcess))
|
||||
return ResourceLimits.Outcome.INSUFFICIENT_GLOBAL;
|
||||
|
||||
if (!endpointReserve.tryAllocate(allocatedExcess))
|
||||
{
|
||||
globalReserve.release(allocatedExcess);
|
||||
globalWaitQueue.signal();
|
||||
return ResourceLimits.Outcome.INSUFFICIENT_ENDPOINT;
|
||||
}
|
||||
|
||||
long newQueueSize = queueSizeUpdater.addAndGet(this, bytes);
|
||||
long actualExcess = max(0, min(newQueueSize - queueCapacity, bytes));
|
||||
|
||||
/*
|
||||
* It's possible that some permits were released at some point after we loaded current queueSize,
|
||||
* and we can satisfy more of the permits using our exclusive per-connection capacity, needing
|
||||
* less than previously estimated from the reserves. If that's the case, release the now unneeded
|
||||
* permit excess back to endpoint/global reserves.
|
||||
*/
|
||||
if (actualExcess != allocatedExcess) // actualExcess < allocatedExcess
|
||||
{
|
||||
long excess = allocatedExcess - actualExcess;
|
||||
|
||||
endpointReserve.release(excess);
|
||||
globalReserve.release(excess);
|
||||
|
||||
endpointWaitQueue.signal();
|
||||
globalWaitQueue.signal();
|
||||
}
|
||||
|
||||
return ResourceLimits.Outcome.SUCCESS;
|
||||
}
|
||||
|
||||
public void releaseCapacity(int bytes)
|
||||
{
|
||||
long oldQueueSize = queueSizeUpdater.getAndAdd(this, -bytes);
|
||||
if (oldQueueSize > queueCapacity)
|
||||
{
|
||||
long excess = min(oldQueueSize - queueCapacity, bytes);
|
||||
|
||||
endpointReserveCapacity.release(excess);
|
||||
globalReserveCapacity.release(excess);
|
||||
|
||||
endpointWaitQueue.signal();
|
||||
globalWaitQueue.signal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to release capacity for a message that has been fully, successfully processed.
|
||||
*
|
||||
* Normally no different from invoking {@link #releaseCapacity(int)}, but is necessary for the verifier
|
||||
* to be able to delay capacity release for backpressure testing.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected void releaseProcessedCapacity(int size, Header header)
|
||||
{
|
||||
releaseCapacity(size);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx)
|
||||
{
|
||||
isClosed = true;
|
||||
|
||||
if (null != largeMessage)
|
||||
largeMessage.abort();
|
||||
|
||||
if (null != ticket)
|
||||
ticket.invalidate();
|
||||
|
||||
onClosed.call(this);
|
||||
}
|
||||
|
||||
private EventLoop eventLoop()
|
||||
{
|
||||
return channel.eventLoop();
|
||||
}
|
||||
|
||||
protected abstract String id();
|
||||
|
||||
/*
|
||||
* A large-message frame-accumulating state machine.
|
||||
*
|
||||
* Collects intact frames until it's has all the bytes necessary to deserialize the large message,
|
||||
* at which point it schedules a task on the appropriate {@link Stage},
|
||||
* a task that deserializes the message and immediately invokes the verb handler.
|
||||
*
|
||||
* Also handles corrupt frames and potential expiry of the large message during accumulation:
|
||||
* if it's taking the frames too long to arrive, there is no point in holding on to the
|
||||
* accumulated frames, or in gathering more - so we release the ones we already have, and
|
||||
* skip any remaining ones, alongside with returning memory permits early.
|
||||
*/
|
||||
protected abstract class LargeMessage<H>
|
||||
{
|
||||
protected final int size;
|
||||
protected final H header;
|
||||
|
||||
protected final List<ShareableBytes> buffers = new ArrayList<>();
|
||||
protected int received;
|
||||
|
||||
protected final long expiresAtNanos;
|
||||
|
||||
protected boolean isExpired;
|
||||
protected boolean isCorrupt;
|
||||
|
||||
protected LargeMessage(int size, H header, long expiresAtNanos, boolean isExpired)
|
||||
{
|
||||
this.size = size;
|
||||
this.header = header;
|
||||
this.expiresAtNanos = expiresAtNanos;
|
||||
this.isExpired = isExpired;
|
||||
}
|
||||
|
||||
protected LargeMessage(int size, H header, long expiresAtNanos, ShareableBytes bytes)
|
||||
{
|
||||
this(size, header, expiresAtNanos, false);
|
||||
buffers.add(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this was the last frame of the large message.
|
||||
*/
|
||||
public boolean supply(Frame frame)
|
||||
{
|
||||
if (frame instanceof IntactFrame)
|
||||
onIntactFrame((IntactFrame) frame);
|
||||
else
|
||||
onCorruptFrame();
|
||||
|
||||
received += frame.frameSize;
|
||||
if (size == received)
|
||||
onComplete();
|
||||
return size == received;
|
||||
}
|
||||
|
||||
private void onIntactFrame(IntactFrame frame)
|
||||
{
|
||||
boolean expires = approxTime.isAfter(expiresAtNanos);
|
||||
if (!isExpired && !isCorrupt)
|
||||
{
|
||||
if (!expires)
|
||||
{
|
||||
buffers.add(frame.contents.sliceAndConsume(frame.frameSize).share());
|
||||
return;
|
||||
}
|
||||
releaseBuffersAndCapacity(); // release resources once we transition from normal state to expired
|
||||
}
|
||||
frame.consume();
|
||||
isExpired |= expires;
|
||||
}
|
||||
|
||||
private void onCorruptFrame()
|
||||
{
|
||||
if (!isExpired && !isCorrupt)
|
||||
releaseBuffersAndCapacity(); // release resources once we transition from normal state to corrupt
|
||||
isCorrupt = true;
|
||||
isExpired |= approxTime.isAfter(expiresAtNanos);
|
||||
}
|
||||
|
||||
protected abstract void onComplete();
|
||||
|
||||
protected abstract void abort();
|
||||
|
||||
protected void releaseBuffers()
|
||||
{
|
||||
buffers.forEach(ShareableBytes::release); buffers.clear();
|
||||
}
|
||||
|
||||
protected void releaseBuffersAndCapacity()
|
||||
{
|
||||
releaseBuffers(); releaseCapacity(size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A special-purpose wait queue to park inbound message handlers that failed to allocate
|
||||
* reserve capacity for a message in. Upon such failure a handler registers itself with
|
||||
* a {@link WaitQueue} of the appropriate kind (either ENDPOINT or GLOBAL - if failed
|
||||
* to allocate endpoint or global reserve capacity, respectively), stops processing any
|
||||
* accumulated frames or receiving new ones, and waits - until reactivated.
|
||||
*
|
||||
* Every time permits are returned to an endpoint or global {@link Limit}, the respective
|
||||
* queue gets signalled, and if there are any handlers registered in it, we will attempt
|
||||
* to reactivate as many waiting handlers as current available reserve capacity allows
|
||||
* us to - immediately, on the {@link #signal()}-calling thread. At most one such attempt
|
||||
* will be in progress at any given time.
|
||||
*
|
||||
* Handlers that can be reactivated will be grouped by their {@link EventLoop} and a single
|
||||
* {@link ReactivateHandlers} task will be scheduled per event loop, on the corresponding
|
||||
* event loops.
|
||||
*
|
||||
* When run, the {@link ReactivateHandlers} task will ask each handler in its group to first
|
||||
* process one message - using preallocated reserve capacity - and if no obstacles were met -
|
||||
* reactivate the handlers, this time using their regular reserves.
|
||||
*
|
||||
* See {@link WaitQueue#schedule()}, {@link ReactivateHandlers#run()}, {@link Ticket#reactivateHandler(Limit)}.
|
||||
*/
|
||||
public static final class WaitQueue
|
||||
{
|
||||
enum Kind { ENDPOINT, GLOBAL }
|
||||
|
||||
private static final int NOT_RUNNING = 0;
|
||||
@SuppressWarnings("unused")
|
||||
private static final int RUNNING = 1;
|
||||
private static final int RUN_AGAIN = 2;
|
||||
|
||||
private volatile int scheduled;
|
||||
private static final AtomicIntegerFieldUpdater<WaitQueue> scheduledUpdater =
|
||||
AtomicIntegerFieldUpdater.newUpdater(WaitQueue.class, "scheduled");
|
||||
|
||||
private final Kind kind;
|
||||
private final Limit reserveCapacity;
|
||||
|
||||
private final ManyToOneConcurrentLinkedQueue<Ticket> queue = new ManyToOneConcurrentLinkedQueue<>();
|
||||
|
||||
private WaitQueue(Kind kind, Limit reserveCapacity)
|
||||
{
|
||||
this.kind = kind;
|
||||
this.reserveCapacity = reserveCapacity;
|
||||
}
|
||||
|
||||
public static WaitQueue endpoint(Limit endpointReserveCapacity)
|
||||
{
|
||||
return new WaitQueue(Kind.ENDPOINT, endpointReserveCapacity);
|
||||
}
|
||||
|
||||
public static WaitQueue global(Limit globalReserveCapacity)
|
||||
{
|
||||
return new WaitQueue(Kind.GLOBAL, globalReserveCapacity);
|
||||
}
|
||||
|
||||
private Ticket register(AbstractMessageHandler handler, int bytesRequested, long registeredAtNanos, long expiresAtNanos)
|
||||
{
|
||||
Ticket ticket = new Ticket(this, handler, bytesRequested, registeredAtNanos, expiresAtNanos);
|
||||
Ticket previous = queue.relaxedPeekLastAndOffer(ticket);
|
||||
if (null == previous || !previous.isWaiting())
|
||||
signal(); // only signal the queue if this handler is first to register
|
||||
return ticket;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void signal()
|
||||
{
|
||||
if (queue.relaxedIsEmpty())
|
||||
return; // we can return early if no handlers have registered with the wait queue
|
||||
|
||||
if (NOT_RUNNING == scheduledUpdater.getAndUpdate(this, i -> min(RUN_AGAIN, i + 1)))
|
||||
{
|
||||
do
|
||||
{
|
||||
schedule();
|
||||
}
|
||||
while (RUN_AGAIN == scheduledUpdater.getAndDecrement(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void schedule()
|
||||
{
|
||||
Map<EventLoop, ReactivateHandlers> tasks = null;
|
||||
|
||||
long currentTimeNanos = approxTime.now();
|
||||
|
||||
Ticket t;
|
||||
while ((t = queue.peek()) != null)
|
||||
{
|
||||
if (!t.call()) // invalidated
|
||||
{
|
||||
queue.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isLive = t.isLive(currentTimeNanos);
|
||||
if (isLive && !reserveCapacity.tryAllocate(t.bytesRequested))
|
||||
{
|
||||
if (!t.reset()) // the ticket was invalidated after being called but before now
|
||||
{
|
||||
queue.remove();
|
||||
continue;
|
||||
}
|
||||
break; // TODO: traverse the entire queue to unblock handlers that have expired or invalidated tickets
|
||||
}
|
||||
|
||||
if (null == tasks)
|
||||
tasks = new IdentityHashMap<>();
|
||||
|
||||
queue.remove();
|
||||
tasks.computeIfAbsent(t.handler.eventLoop(), e -> new ReactivateHandlers()).add(t, isLive);
|
||||
}
|
||||
|
||||
if (null != tasks)
|
||||
tasks.forEach(EventLoop::execute);
|
||||
}
|
||||
|
||||
private class ReactivateHandlers implements Runnable
|
||||
{
|
||||
List<Ticket> tickets = new ArrayList<>();
|
||||
long capacity = 0L;
|
||||
|
||||
private void add(Ticket ticket, boolean isLive)
|
||||
{
|
||||
tickets.add(ticket);
|
||||
if (isLive) capacity += ticket.bytesRequested;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
Limit limit = new ResourceLimits.Basic(capacity);
|
||||
try
|
||||
{
|
||||
for (Ticket ticket : tickets)
|
||||
ticket.reactivateHandler(limit);
|
||||
}
|
||||
finally
|
||||
{
|
||||
/*
|
||||
* Free up any unused capacity, if any. Will be non-zero if one or more handlers were closed
|
||||
* when we attempted to run their callback, or used more of their other reserve; or if the first
|
||||
* message in the unprocessed stream has expired in the narrow time window.
|
||||
*/
|
||||
long remaining = limit.remaining();
|
||||
if (remaining > 0)
|
||||
{
|
||||
reserveCapacity.release(remaining);
|
||||
signal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Ticket
|
||||
{
|
||||
private static final int WAITING = 0;
|
||||
private static final int CALLED = 1;
|
||||
private static final int INVALIDATED = 2; // invalidated by a handler that got closed
|
||||
|
||||
private volatile int state;
|
||||
private static final AtomicIntegerFieldUpdater<Ticket> stateUpdater =
|
||||
AtomicIntegerFieldUpdater.newUpdater(Ticket.class, "state");
|
||||
|
||||
private final WaitQueue waitQueue;
|
||||
private final AbstractMessageHandler handler;
|
||||
private final int bytesRequested;
|
||||
private final long reigsteredAtNanos;
|
||||
private final long expiresAtNanos;
|
||||
|
||||
private Ticket(WaitQueue waitQueue, AbstractMessageHandler handler, int bytesRequested, long registeredAtNanos, long expiresAtNanos)
|
||||
{
|
||||
this.waitQueue = waitQueue;
|
||||
this.handler = handler;
|
||||
this.bytesRequested = bytesRequested;
|
||||
this.reigsteredAtNanos = registeredAtNanos;
|
||||
this.expiresAtNanos = expiresAtNanos;
|
||||
}
|
||||
|
||||
private void reactivateHandler(Limit capacity)
|
||||
{
|
||||
long elapsedNanos = approxTime.now() - reigsteredAtNanos;
|
||||
try
|
||||
{
|
||||
if (waitQueue.kind == Kind.ENDPOINT)
|
||||
handler.onEndpointReserveCapacityRegained(capacity, elapsedNanos);
|
||||
else
|
||||
handler.onGlobalReserveCapacityRegained(capacity, elapsedNanos);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
logger.error("{} exception caught while reactivating a handler", handler.id(), t);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWaiting()
|
||||
{
|
||||
return state == WAITING;
|
||||
}
|
||||
|
||||
private boolean isLive(long currentTimeNanos)
|
||||
{
|
||||
return !approxTime.isAfter(currentTimeNanos, expiresAtNanos);
|
||||
}
|
||||
|
||||
private void invalidate()
|
||||
{
|
||||
state = INVALIDATED;
|
||||
waitQueue.signal();
|
||||
}
|
||||
|
||||
private boolean call()
|
||||
{
|
||||
return stateUpdater.compareAndSet(this, WAITING, CALLED);
|
||||
}
|
||||
|
||||
private boolean reset()
|
||||
{
|
||||
return stateUpdater.compareAndSet(this, CALLED, WAITING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnHandlerClosed
|
||||
{
|
||||
void call(AbstractMessageHandler handler);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ import java.util.zip.CRC32;
|
|||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.util.concurrent.FastThreadLocal;
|
||||
|
||||
class Crc
|
||||
public class Crc
|
||||
{
|
||||
private static final FastThreadLocal<CRC32> crc32 = new FastThreadLocal<CRC32>()
|
||||
{
|
||||
|
|
@ -37,9 +37,9 @@ class Crc
|
|||
|
||||
private static final byte[] initialBytes = new byte[] { (byte) 0xFA, (byte) 0x2D, (byte) 0x55, (byte) 0xCA };
|
||||
|
||||
static final class InvalidCrc extends IOException
|
||||
public static final class InvalidCrc extends IOException
|
||||
{
|
||||
InvalidCrc(int read, int computed)
|
||||
public InvalidCrc(int read, int computed)
|
||||
{
|
||||
super(String.format("Read %d, Computed %d", read, computed));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ import static org.apache.cassandra.utils.ByteBufferUtil.copyBytes;
|
|||
* 5. {@link FrameDecoderLegacyLZ4}
|
||||
* LZ4 compression using standard LZ4 frame format; groups legacy messages (< 4.0) into frames
|
||||
*/
|
||||
abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
||||
public abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
||||
{
|
||||
private static final FrameProcessor NO_PROCESSOR =
|
||||
frame -> { throw new IllegalStateException("Frame processor invoked on an unregistered FrameDecoder"); };
|
||||
|
|
@ -64,7 +64,7 @@ abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
|||
private static final FrameProcessor CLOSED_PROCESSOR =
|
||||
frame -> { throw new IllegalStateException("Frame processor invoked on a closed FrameDecoder"); };
|
||||
|
||||
interface FrameProcessor
|
||||
public interface FrameProcessor
|
||||
{
|
||||
/**
|
||||
* Frame processor that the frames should be handed off to.
|
||||
|
|
@ -75,10 +75,10 @@ abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
|||
boolean process(Frame frame) throws IOException;
|
||||
}
|
||||
|
||||
abstract static class Frame
|
||||
public abstract static class Frame
|
||||
{
|
||||
final boolean isSelfContained;
|
||||
final int frameSize;
|
||||
public final boolean isSelfContained;
|
||||
public final int frameSize;
|
||||
|
||||
Frame(boolean isSelfContained, int frameSize)
|
||||
{
|
||||
|
|
@ -99,9 +99,9 @@ abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
|||
* {@link Message} is contained in the payload; it can be relied upon that this partial {@link Message}
|
||||
* will only be delivered in its own unique {@link Frame}.
|
||||
*/
|
||||
final static class IntactFrame extends Frame
|
||||
public final static class IntactFrame extends Frame
|
||||
{
|
||||
final ShareableBytes contents;
|
||||
public final ShareableBytes contents;
|
||||
|
||||
IntactFrame(boolean isSelfContained, ShareableBytes contents)
|
||||
{
|
||||
|
|
@ -119,7 +119,7 @@ abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
|||
return !contents.hasRemaining();
|
||||
}
|
||||
|
||||
void consume()
|
||||
public void consume()
|
||||
{
|
||||
contents.consume();
|
||||
}
|
||||
|
|
@ -136,9 +136,10 @@ abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
|||
* A recoverable {@link CorruptFrame} can be considered unrecoverable by {@link InboundMessageHandler}
|
||||
* if it's the first frame of a large message (isn't self contained).
|
||||
*/
|
||||
final static class CorruptFrame extends Frame
|
||||
public final static class CorruptFrame extends Frame
|
||||
{
|
||||
final int readCRC, computedCRC;
|
||||
public final int readCRC;
|
||||
public final int computedCRC;
|
||||
|
||||
CorruptFrame(boolean isSelfContained, int frameSize, int readCRC, int computedCRC)
|
||||
{
|
||||
|
|
@ -157,7 +158,7 @@ abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
|||
return new CorruptFrame(false, Integer.MIN_VALUE, readCRC, computedCRC);
|
||||
}
|
||||
|
||||
boolean isRecoverable()
|
||||
public boolean isRecoverable()
|
||||
{
|
||||
return frameSize != Integer.MIN_VALUE;
|
||||
}
|
||||
|
|
@ -192,7 +193,7 @@ abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
|||
/**
|
||||
* For use by InboundMessageHandler (or other upstream handlers) that want to start receiving frames.
|
||||
*/
|
||||
void activate(FrameProcessor processor)
|
||||
public void activate(FrameProcessor processor)
|
||||
{
|
||||
if (this.processor != NO_PROCESSOR)
|
||||
throw new IllegalStateException("Attempted to activate an already active FrameDecoder");
|
||||
|
|
@ -235,7 +236,7 @@ abstract class FrameDecoder extends ChannelInboundHandlerAdapter
|
|||
* For use by InboundMessageHandler (or other upstream handlers) that want to permanently
|
||||
* stop receiving frames, e.g. because of an exception caught.
|
||||
*/
|
||||
void discard()
|
||||
public void discard()
|
||||
{
|
||||
isActive = false;
|
||||
processor = CLOSED_PROCESSOR;
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@ import static org.apache.cassandra.net.Crc.updateCrc32;
|
|||
* | CRC32 of Payload |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
final class FrameDecoderCrc extends FrameDecoderWith8bHeader
|
||||
public final class FrameDecoderCrc extends FrameDecoderWith8bHeader
|
||||
{
|
||||
private FrameDecoderCrc(BufferPoolAllocator allocator)
|
||||
public FrameDecoderCrc(BufferPoolAllocator allocator)
|
||||
{
|
||||
super(allocator);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ import static org.apache.cassandra.net.Crc.*;
|
|||
* | CRC32 of Compressed Payload |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
final class FrameDecoderLZ4 extends FrameDecoderWith8bHeader
|
||||
public final class FrameDecoderLZ4 extends FrameDecoderWith8bHeader
|
||||
{
|
||||
public static FrameDecoderLZ4 fast(BufferPoolAllocator allocator)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import org.apache.cassandra.io.compress.BufferType;
|
|||
import org.apache.cassandra.utils.memory.BufferPool;
|
||||
import org.apache.cassandra.utils.memory.BufferPools;
|
||||
|
||||
abstract class FrameEncoder extends ChannelOutboundHandlerAdapter
|
||||
public abstract class FrameEncoder extends ChannelOutboundHandlerAdapter
|
||||
{
|
||||
protected static final BufferPool bufferPool = BufferPools.forNetworking();
|
||||
|
||||
|
|
@ -36,12 +36,13 @@ abstract class FrameEncoder extends ChannelOutboundHandlerAdapter
|
|||
* of the {@code FrameEncoder} without knowledge of the encoder's frame layout, while ensuring
|
||||
* enough space to write the remainder of the frame's contents is reserved.
|
||||
*/
|
||||
static class Payload
|
||||
public static class Payload
|
||||
{
|
||||
public static final int MAX_SIZE = 1 << 17;
|
||||
// isSelfContained is a flag in the Frame API, indicating if the contents consists of only complete messages
|
||||
private boolean isSelfContained;
|
||||
// the buffer to write to
|
||||
final ByteBuffer buffer;
|
||||
public final ByteBuffer buffer;
|
||||
// the number of header bytes to reserve
|
||||
final int headerLength;
|
||||
// the number of trailer bytes to reserve
|
||||
|
|
@ -71,13 +72,6 @@ abstract class FrameEncoder extends ChannelOutboundHandlerAdapter
|
|||
this.isSelfContained = isSelfContained;
|
||||
}
|
||||
|
||||
// do not invoke after finish()
|
||||
boolean isEmpty()
|
||||
{
|
||||
assert !isFinished;
|
||||
return buffer.position() == headerLength;
|
||||
}
|
||||
|
||||
// do not invoke after finish()
|
||||
int length()
|
||||
{
|
||||
|
|
@ -86,7 +80,7 @@ abstract class FrameEncoder extends ChannelOutboundHandlerAdapter
|
|||
}
|
||||
|
||||
// do not invoke after finish()
|
||||
int remaining()
|
||||
public int remaining()
|
||||
{
|
||||
assert !isFinished;
|
||||
return buffer.remaining();
|
||||
|
|
@ -100,7 +94,7 @@ abstract class FrameEncoder extends ChannelOutboundHandlerAdapter
|
|||
}
|
||||
|
||||
// may not be written to or queried, after this is invoked; must be passed straight to an encoder (or release called)
|
||||
void finish()
|
||||
public void finish()
|
||||
{
|
||||
assert !isFinished;
|
||||
isFinished = true;
|
||||
|
|
@ -109,19 +103,19 @@ abstract class FrameEncoder extends ChannelOutboundHandlerAdapter
|
|||
bufferPool.putUnusedPortion(buffer);
|
||||
}
|
||||
|
||||
void release()
|
||||
public void release()
|
||||
{
|
||||
bufferPool.put(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
interface PayloadAllocator
|
||||
public interface PayloadAllocator
|
||||
{
|
||||
public static final PayloadAllocator simple = Payload::new;
|
||||
Payload allocate(boolean isSelfContained, int capacity);
|
||||
}
|
||||
|
||||
PayloadAllocator allocator()
|
||||
public PayloadAllocator allocator()
|
||||
{
|
||||
return PayloadAllocator.simple;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,17 +31,17 @@ import static org.apache.cassandra.net.Crc.*;
|
|||
* Please see {@link FrameDecoderCrc} for description of the framing produced by this encoder.
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
class FrameEncoderCrc extends FrameEncoder
|
||||
public class FrameEncoderCrc extends FrameEncoder
|
||||
{
|
||||
static final int HEADER_LENGTH = 6;
|
||||
private static final int TRAILER_LENGTH = 4;
|
||||
static final int HEADER_AND_TRAILER_LENGTH = 10;
|
||||
public static final int HEADER_AND_TRAILER_LENGTH = 10;
|
||||
|
||||
static final FrameEncoderCrc instance = new FrameEncoderCrc();
|
||||
public static final FrameEncoderCrc instance = new FrameEncoderCrc();
|
||||
static final PayloadAllocator allocator = (isSelfContained, capacity) ->
|
||||
new Payload(isSelfContained, capacity, HEADER_LENGTH, TRAILER_LENGTH);
|
||||
|
||||
PayloadAllocator allocator()
|
||||
public PayloadAllocator allocator()
|
||||
{
|
||||
return allocator;
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ class FrameEncoderCrc extends FrameEncoder
|
|||
if (isSelfContained)
|
||||
header3b |= 1 << 17;
|
||||
int crc = crc24(header3b, 3);
|
||||
|
||||
put3b(frame, 0, header3b);
|
||||
put3b(frame, 3, crc);
|
||||
}
|
||||
|
|
@ -87,6 +86,7 @@ class FrameEncoderCrc extends FrameEncoder
|
|||
frame.limit(frameLength);
|
||||
frame.putInt(frameLength - TRAILER_LENGTH, frameCrc);
|
||||
frame.position(0);
|
||||
|
||||
return GlobalBufferPoolAllocator.wrap(frame);
|
||||
}
|
||||
catch (Throwable t)
|
||||
|
|
|
|||
|
|
@ -34,9 +34,10 @@ import static org.apache.cassandra.net.Crc.*;
|
|||
* Please see {@link FrameDecoderLZ4} for description of the framing produced by this encoder.
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
public
|
||||
class FrameEncoderLZ4 extends FrameEncoder
|
||||
{
|
||||
static final FrameEncoderLZ4 fastInstance = new FrameEncoderLZ4(LZ4Factory.fastestInstance().fastCompressor());
|
||||
public static final FrameEncoderLZ4 fastInstance = new FrameEncoderLZ4(LZ4Factory.fastestInstance().fastCompressor());
|
||||
|
||||
private final LZ4Compressor compressor;
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ class FrameEncoderLZ4 extends FrameEncoder
|
|||
}
|
||||
|
||||
private static final int HEADER_LENGTH = 8;
|
||||
static final int HEADER_AND_TRAILER_LENGTH = 12;
|
||||
public static final int HEADER_AND_TRAILER_LENGTH = 12;
|
||||
|
||||
private static void writeHeader(ByteBuffer frame, boolean isSelfContained, long compressedLength, long uncompressedLength)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class FrameEncoderUnprotected extends FrameEncoder
|
|||
static final PayloadAllocator allocator = (isSelfContained, capacity) ->
|
||||
new Payload(isSelfContained, capacity, HEADER_LENGTH, 0);
|
||||
|
||||
PayloadAllocator allocator()
|
||||
public PayloadAllocator allocator()
|
||||
{
|
||||
return allocator;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ import org.apache.cassandra.utils.memory.BufferPool;
|
|||
/**
|
||||
* Primary {@link ByteBuf} / {@link ByteBuffer} allocator - using the global {@link BufferPool}.
|
||||
*/
|
||||
class GlobalBufferPoolAllocator extends BufferPoolAllocator
|
||||
public class GlobalBufferPoolAllocator extends BufferPoolAllocator
|
||||
{
|
||||
static final GlobalBufferPoolAllocator instance = new GlobalBufferPoolAllocator();
|
||||
public static final GlobalBufferPoolAllocator instance = new GlobalBufferPoolAllocator();
|
||||
|
||||
private GlobalBufferPoolAllocator()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,23 +20,14 @@ package org.apache.cassandra.net;
|
|||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
|
||||
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.EventLoop;
|
||||
import org.apache.cassandra.concurrent.ExecutorLocals;
|
||||
import org.apache.cassandra.concurrent.Stage;
|
||||
import org.apache.cassandra.exceptions.IncompatibleSchemaException;
|
||||
|
|
@ -53,135 +44,44 @@ import org.apache.cassandra.tracing.Tracing;
|
|||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
import static java.util.concurrent.TimeUnit.NANOSECONDS;
|
||||
import static org.apache.cassandra.net.Crc.*;
|
||||
import static org.apache.cassandra.utils.MonotonicClock.approxTime;
|
||||
|
||||
/**
|
||||
* Core logic for handling inbound message deserialization and execution (in tandem with {@link FrameDecoder}).
|
||||
*
|
||||
* Handles small and large messages, corruption, flow control, dispatch of message processing onto an appropriate
|
||||
* thread pool.
|
||||
*
|
||||
* # Interaction with {@link FrameDecoder}
|
||||
*
|
||||
* {@link InboundMessageHandler} sits on top of a {@link FrameDecoder} in the Netty pipeline, and is tightly
|
||||
* coupled with it.
|
||||
*
|
||||
* {@link FrameDecoder} decodes inbound frames and relies on a supplied {@link FrameProcessor} to act on them.
|
||||
* {@link InboundMessageHandler} provides two implementations of that interface:
|
||||
* - {@link #process(Frame)} is the default, primary processor, and the primary entry point to this class
|
||||
* - {@link UpToOneMessageFrameProcessor}, supplied to the decoder when the handler is reactivated after being
|
||||
* put in waiting mode due to lack of acquirable reserve memory capacity permits
|
||||
*
|
||||
* Return value of {@link FrameProcessor#process(Frame)} determines whether the decoder should keep processing
|
||||
* frames (if {@code true} is returned) or stop until explicitly reactivated (if {@code false} is). To reactivate
|
||||
* the decoder (once notified of available resource permits), {@link FrameDecoder#reactivate()} is invoked.
|
||||
*
|
||||
* # Frames
|
||||
*
|
||||
* {@link InboundMessageHandler} operates on frames of messages, and there are several kinds of them:
|
||||
* 1. {@link IntactFrame} that are contained. As names suggest, these contain one or multiple fully contained
|
||||
* messages believed to be uncorrupted. Guaranteed to not contain an part of an incomplete message.
|
||||
* See {@link #processFrameOfContainedMessages(ShareableBytes, Limit, Limit)}.
|
||||
* 2. {@link IntactFrame} that are NOT contained. These are uncorrupted parts of a large message split over multiple
|
||||
* parts due to their size. Can represent first or subsequent frame of a large message.
|
||||
* See {@link #processFirstFrameOfLargeMessage(IntactFrame, Limit, Limit)} and
|
||||
* {@link #processSubsequentFrameOfLargeMessage(Frame)}.
|
||||
* 3. {@link CorruptFrame} with corrupt header. These are unrecoverable, and force a connection to be dropped.
|
||||
* 4. {@link CorruptFrame} with a valid header, but corrupt payload. These can be either contained or uncontained.
|
||||
* - contained frames with corrupt payload can be gracefully dropped without dropping the connection
|
||||
* - uncontained frames with corrupt payload can be gracefully dropped unless they represent the first
|
||||
* frame of a new large message, as in that case we don't know how many bytes to skip
|
||||
* See {@link #processCorruptFrame(CorruptFrame)}.
|
||||
*
|
||||
* Fundamental frame invariants:
|
||||
* 1. A contained frame can only have fully-encapsulated messages - 1 to n, that don't cross frame boundaries
|
||||
* 2. An uncontained frame can hold a part of one message only. It can NOT, say, contain end of one large message
|
||||
* and a beginning of another one. All the bytes in an uncontained frame always belong to a single message.
|
||||
* Implementation of {@link AbstractMessageHandler} for processing internode messages from peers.
|
||||
*
|
||||
* # Small vs large messages
|
||||
*
|
||||
* A single handler is equipped to process both small and large messages, potentially interleaved, but the logic
|
||||
* differs depending on size. Small messages are deserialized in place, and then handed off to an appropriate
|
||||
* Small messages are deserialized in place, and then handed off to an appropriate
|
||||
* thread pool for processing. Large messages accumulate frames until completion of a message, then hand off
|
||||
* the untouched frames to the correct thread pool for the verb to be deserialized there and immediately processed.
|
||||
*
|
||||
* See {@link LargeMessage} for details of the large-message accumulating state-machine, and {@link ProcessMessage}
|
||||
* and its inheritors for the differences in execution.
|
||||
*
|
||||
* # Flow control (backpressure)
|
||||
*
|
||||
* To prevent nodes from overwhelming and bringing each other to the knees with more inbound messages that
|
||||
* can be processed in a timely manner, {@link InboundMessageHandler} implements a strict flow control policy.
|
||||
* The size of the incoming message is dependent on the messaging version of the specific peer connection. See
|
||||
* {@link Message.Serializer#inferMessageSize(ByteBuffer, int, int, int)}.
|
||||
*
|
||||
* Before we attempt to process a message fully, we first infer its size from the stream. Then we attempt to
|
||||
* acquire memory permits for a message of that size. If we succeed, then we move on actually process the message.
|
||||
* If we fail, the frame decoder deactivates until sufficient permits are released for the message to be processed
|
||||
* and the handler is activated again. Permits are released back once the message has been fully processed -
|
||||
* after the verb handler has been invoked - on the {@link Stage} for the {@link Verb} of the message.
|
||||
* By default, every connection has 4MiB of exlusive permits available before needing to access the per-endpoint
|
||||
* and global reserves.
|
||||
*
|
||||
* Every connection has an exclusive number of permits allocated to it (by default 4MiB). In addition to it,
|
||||
* there is a per-endpoint reserve capacity and a global reserve capacity {@link Limit}, shared between all
|
||||
* connections from the same host and all connections, respectively. So long as long as the handler stays within
|
||||
* its exclusive limit, it doesn't need to tap into reserve capacity.
|
||||
*
|
||||
* If tapping into reserve capacity is necessary, but the handler fails to acquire capacity from either
|
||||
* endpoint of global reserve (and it needs to acquire from both), the handler and its frame decoder become
|
||||
* inactive and register with a {@link WaitQueue} of the appropriate type, depending on which of the reserves
|
||||
* couldn't be tapped into. Once enough messages have finished processing and had their permits released back
|
||||
* to the reserves, {@link WaitQueue} will reactivate the sleeping handlers and they'll resume processing frames.
|
||||
*
|
||||
* The reason we 'split' reserve capacity into two limits - endpoing and global - is to guarantee liveness, and
|
||||
* prevent single endpoint's connections from taking over the whole reserve, starving other connections.
|
||||
*
|
||||
* One permit per byte of serialized message gets acquired. When inflated on-heap, each message will occupy more
|
||||
* than that, necessarily, but despite wide variance, it's a good enough proxy that correlates with on-heap footprint.
|
||||
* Permits are released after the verb handler has been invoked on the {@link Stage} for the {@link Verb} of the message.
|
||||
*/
|
||||
public class InboundMessageHandler extends ChannelInboundHandlerAdapter implements FrameProcessor
|
||||
public class InboundMessageHandler extends AbstractMessageHandler
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(InboundMessageHandler.class);
|
||||
private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1L, TimeUnit.SECONDS);
|
||||
|
||||
private static final Message.Serializer serializer = Message.serializer;
|
||||
|
||||
private final FrameDecoder decoder;
|
||||
|
||||
private final ConnectionType type;
|
||||
private final Channel channel;
|
||||
private final InetAddressAndPort self;
|
||||
private final InetAddressAndPort peer;
|
||||
private final int version;
|
||||
|
||||
private final int largeThreshold;
|
||||
private LargeMessage largeMessage;
|
||||
|
||||
private final long queueCapacity;
|
||||
volatile long queueSize = 0L;
|
||||
private static final AtomicLongFieldUpdater<InboundMessageHandler> queueSizeUpdater =
|
||||
AtomicLongFieldUpdater.newUpdater(InboundMessageHandler.class, "queueSize");
|
||||
|
||||
private final Limit endpointReserveCapacity;
|
||||
private final WaitQueue endpointWaitQueue;
|
||||
|
||||
private final Limit globalReserveCapacity;
|
||||
private final WaitQueue globalWaitQueue;
|
||||
|
||||
private final OnHandlerClosed onClosed;
|
||||
private final InboundMessageCallbacks callbacks;
|
||||
private final Consumer<Message<?>> consumer;
|
||||
|
||||
// wait queue handle, non-null if we overrun endpoint or global capacity and request to be resumed once it's released
|
||||
private WaitQueue.Ticket ticket = null;
|
||||
|
||||
long corruptFramesRecovered, corruptFramesUnrecovered;
|
||||
long receivedCount, receivedBytes;
|
||||
long throttledCount, throttledNanos;
|
||||
|
||||
private boolean isClosed;
|
||||
|
||||
InboundMessageHandler(FrameDecoder decoder,
|
||||
|
||||
ConnectionType type,
|
||||
|
|
@ -201,78 +101,26 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
InboundMessageCallbacks callbacks,
|
||||
Consumer<Message<?>> consumer)
|
||||
{
|
||||
this.decoder = decoder;
|
||||
super(decoder,
|
||||
channel,
|
||||
largeThreshold,
|
||||
queueCapacity,
|
||||
endpointReserveCapacity,
|
||||
globalReserveCapacity,
|
||||
endpointWaitQueue,
|
||||
globalWaitQueue,
|
||||
onClosed);
|
||||
|
||||
|
||||
this.type = type;
|
||||
this.channel = channel;
|
||||
this.self = self;
|
||||
this.peer = peer;
|
||||
this.version = version;
|
||||
this.largeThreshold = largeThreshold;
|
||||
|
||||
this.queueCapacity = queueCapacity;
|
||||
this.endpointReserveCapacity = endpointReserveCapacity;
|
||||
this.endpointWaitQueue = endpointWaitQueue;
|
||||
this.globalReserveCapacity = globalReserveCapacity;
|
||||
this.globalWaitQueue = globalWaitQueue;
|
||||
|
||||
this.onClosed = onClosed;
|
||||
this.callbacks = callbacks;
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg)
|
||||
{
|
||||
/*
|
||||
* InboundMessageHandler works in tandem with FrameDecoder to implement flow control
|
||||
* and work stashing optimally. We rely on FrameDecoder to invoke the provided
|
||||
* FrameProcessor rather than on the pipeline and invocations of channelRead().
|
||||
* process(Frame) is the primary entry point for this class.
|
||||
*/
|
||||
throw new IllegalStateException("InboundMessageHandler doesn't expect channelRead() to be invoked");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx)
|
||||
{
|
||||
decoder.activate(this); // the frame decoder starts inactive until explicitly activated by the added inbound message handler
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(Frame frame) throws IOException
|
||||
{
|
||||
if (frame instanceof IntactFrame)
|
||||
return processIntactFrame((IntactFrame) frame, endpointReserveCapacity, globalReserveCapacity);
|
||||
|
||||
processCorruptFrame((CorruptFrame) frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean processIntactFrame(IntactFrame frame, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
if (frame.isSelfContained)
|
||||
return processFrameOfContainedMessages(frame.contents, endpointReserve, globalReserve);
|
||||
else if (null == largeMessage)
|
||||
return processFirstFrameOfLargeMessage(frame, endpointReserve, globalReserve);
|
||||
else
|
||||
return processSubsequentFrameOfLargeMessage(frame);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle contained messages (not crossing boundaries of the frame) - both small and large, for the inbound
|
||||
* definition of large (breaching the size threshold for what we are willing to process on event-loop vs.
|
||||
* off event-loop).
|
||||
*/
|
||||
private boolean processFrameOfContainedMessages(ShareableBytes bytes, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
while (bytes.hasRemaining())
|
||||
if (!processOneContainedMessage(bytes, endpointReserve, globalReserve))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean processOneContainedMessage(ShareableBytes bytes, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
protected boolean processOneContainedMessage(ShareableBytes bytes, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
ByteBuffer buf = bytes.get();
|
||||
|
||||
|
|
@ -358,7 +206,7 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
* Handling of multi-frame large messages
|
||||
*/
|
||||
|
||||
private boolean processFirstFrameOfLargeMessage(IntactFrame frame, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
protected boolean processFirstFrameOfLargeMessage(IntactFrame frame, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
ShareableBytes bytes = frame.contents;
|
||||
ByteBuffer buf = bytes.get();
|
||||
|
|
@ -378,305 +226,34 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
return true;
|
||||
}
|
||||
|
||||
private boolean processSubsequentFrameOfLargeMessage(Frame frame)
|
||||
{
|
||||
receivedBytes += frame.frameSize;
|
||||
if (largeMessage.supply(frame))
|
||||
{
|
||||
receivedCount++;
|
||||
largeMessage = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* We can handle some corrupt frames gracefully without dropping the connection and losing all the
|
||||
* queued up messages, but not others.
|
||||
*
|
||||
* Corrupt frames that *ARE NOT* safe to skip gracefully and require the connection to be dropped:
|
||||
* - any frame with corrupt header (!frame.isRecoverable())
|
||||
* - first corrupt-payload frame of a large message (impossible to infer message size, and without it
|
||||
* impossible to skip the message safely
|
||||
*
|
||||
* Corrupt frames that *ARE* safe to skip gracefully, without reconnecting:
|
||||
* - any self-contained frame with a corrupt payload (but not header): we lose all the messages in the
|
||||
* frame, but that has no effect on subsequent ones
|
||||
* - any non-first payload-corrupt frame of a large message: we know the size of the large message in
|
||||
* flight, so we just skip frames until we've seen all its bytes; we only lose the large message
|
||||
*/
|
||||
private void processCorruptFrame(CorruptFrame frame) throws InvalidCrc
|
||||
protected void processCorruptFrame(CorruptFrame frame) throws Crc.InvalidCrc
|
||||
{
|
||||
if (!frame.isRecoverable())
|
||||
{
|
||||
corruptFramesUnrecovered++;
|
||||
throw new InvalidCrc(frame.readCRC, frame.computedCRC);
|
||||
throw new Crc.InvalidCrc(frame.readCRC, frame.computedCRC);
|
||||
}
|
||||
else if (frame.isSelfContained)
|
||||
{
|
||||
receivedBytes += frame.frameSize;
|
||||
corruptFramesRecovered++;
|
||||
noSpamLogger.warn("{} invalid, recoverable CRC mismatch detected while reading messages (corrupted self-contained frame)", this);
|
||||
noSpamLogger.warn("{} invalid, recoverable CRC mismatch detected while reading messages (corrupted self-contained frame)", id());
|
||||
}
|
||||
else if (null == largeMessage) // first frame of a large message
|
||||
{
|
||||
receivedBytes += frame.frameSize;
|
||||
corruptFramesUnrecovered++;
|
||||
noSpamLogger.error("{} invalid, unrecoverable CRC mismatch detected while reading messages (corrupted first frame of a large message)", this);
|
||||
throw new InvalidCrc(frame.readCRC, frame.computedCRC);
|
||||
noSpamLogger.error("{} invalid, unrecoverable CRC mismatch detected while reading messages (corrupted first frame of a large message)", id());
|
||||
throw new Crc.InvalidCrc(frame.readCRC, frame.computedCRC);
|
||||
}
|
||||
else // subsequent frame of a large message
|
||||
{
|
||||
processSubsequentFrameOfLargeMessage(frame);
|
||||
corruptFramesRecovered++;
|
||||
noSpamLogger.warn("{} invalid, recoverable CRC mismatch detected while reading a large message", this);
|
||||
noSpamLogger.warn("{} invalid, recoverable CRC mismatch detected while reading a large message", id());
|
||||
}
|
||||
}
|
||||
|
||||
private void onEndpointReserveCapacityRegained(Limit endpointReserve, long elapsedNanos)
|
||||
{
|
||||
onReserveCapacityRegained(endpointReserve, globalReserveCapacity, elapsedNanos);
|
||||
}
|
||||
|
||||
private void onGlobalReserveCapacityRegained(Limit globalReserve, long elapsedNanos)
|
||||
{
|
||||
onReserveCapacityRegained(endpointReserveCapacity, globalReserve, elapsedNanos);
|
||||
}
|
||||
|
||||
private void onReserveCapacityRegained(Limit endpointReserve, Limit globalReserve, long elapsedNanos)
|
||||
{
|
||||
if (isClosed)
|
||||
return;
|
||||
|
||||
assert channel.eventLoop().inEventLoop();
|
||||
|
||||
ticket = null;
|
||||
throttledNanos += elapsedNanos;
|
||||
|
||||
try
|
||||
{
|
||||
/*
|
||||
* Process up to one message using supplied overriden reserves - one of them pre-allocated,
|
||||
* and guaranteed to be enough for one message - then, if no obstacles enountered, reactivate
|
||||
* the frame decoder using normal reserve capacities.
|
||||
*/
|
||||
if (processUpToOneMessage(endpointReserve, globalReserve))
|
||||
decoder.reactivate();
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
exceptionCaught(t);
|
||||
}
|
||||
}
|
||||
|
||||
// return true if the handler should be reactivated - if no new hurdles were encountered,
|
||||
// like running out of the other kind of reserve capacity
|
||||
private boolean processUpToOneMessage(Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
UpToOneMessageFrameProcessor processor = new UpToOneMessageFrameProcessor(endpointReserve, globalReserve);
|
||||
decoder.processBacklog(processor);
|
||||
return processor.isActive;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process at most one message. Won't always be an entire one (if the message in the head of line
|
||||
* is a large one, and there aren't sufficient frames to decode it entirely), but will never be more than one.
|
||||
*/
|
||||
private class UpToOneMessageFrameProcessor implements FrameProcessor
|
||||
{
|
||||
private final Limit endpointReserve;
|
||||
private final Limit globalReserve;
|
||||
|
||||
boolean isActive = true;
|
||||
boolean firstFrame = true;
|
||||
|
||||
private UpToOneMessageFrameProcessor(Limit endpointReserve, Limit globalReserve)
|
||||
{
|
||||
this.endpointReserve = endpointReserve;
|
||||
this.globalReserve = globalReserve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(Frame frame) throws IOException
|
||||
{
|
||||
if (firstFrame)
|
||||
{
|
||||
if (!(frame instanceof IntactFrame))
|
||||
throw new IllegalStateException("First backlog frame must be intact");
|
||||
firstFrame = false;
|
||||
return processFirstFrame((IntactFrame) frame);
|
||||
}
|
||||
|
||||
return processSubsequentFrame(frame);
|
||||
}
|
||||
|
||||
private boolean processFirstFrame(IntactFrame frame) throws IOException
|
||||
{
|
||||
if (frame.isSelfContained)
|
||||
{
|
||||
isActive = processOneContainedMessage(frame.contents, endpointReserve, globalReserve);
|
||||
return false; // stop after one message
|
||||
}
|
||||
else
|
||||
{
|
||||
isActive = processFirstFrameOfLargeMessage(frame, endpointReserve, globalReserve);
|
||||
return isActive; // continue unless fallen behind coprocessor or ran out of reserve capacity again
|
||||
}
|
||||
}
|
||||
|
||||
private boolean processSubsequentFrame(Frame frame) throws IOException
|
||||
{
|
||||
if (frame instanceof IntactFrame)
|
||||
processSubsequentFrameOfLargeMessage(frame);
|
||||
else
|
||||
processCorruptFrame((CorruptFrame) frame);
|
||||
|
||||
return largeMessage != null; // continue until done with the large message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to acquire permits for the inbound message. In case of failure, register with the right wait queue to be
|
||||
* reactivated once permit capacity is regained.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private boolean acquireCapacity(Limit endpointReserve, Limit globalReserve, int bytes, long currentTimeNanos, long expiresAtNanos)
|
||||
{
|
||||
ResourceLimits.Outcome outcome = acquireCapacity(endpointReserve, globalReserve, bytes);
|
||||
|
||||
if (outcome == ResourceLimits.Outcome.INSUFFICIENT_ENDPOINT)
|
||||
ticket = endpointWaitQueue.register(this, bytes, currentTimeNanos, expiresAtNanos);
|
||||
else if (outcome == ResourceLimits.Outcome.INSUFFICIENT_GLOBAL)
|
||||
ticket = globalWaitQueue.register(this, bytes, currentTimeNanos, expiresAtNanos);
|
||||
|
||||
if (outcome != ResourceLimits.Outcome.SUCCESS)
|
||||
throttledCount++;
|
||||
|
||||
return outcome == ResourceLimits.Outcome.SUCCESS;
|
||||
}
|
||||
|
||||
private ResourceLimits.Outcome acquireCapacity(Limit endpointReserve, Limit globalReserve, int bytes)
|
||||
{
|
||||
long currentQueueSize = queueSize;
|
||||
|
||||
/*
|
||||
* acquireCapacity() is only ever called on the event loop, and as such queueSize is only ever increased
|
||||
* on the event loop. If there is enough capacity, we can safely addAndGet() and immediately return.
|
||||
*/
|
||||
if (currentQueueSize + bytes <= queueCapacity)
|
||||
{
|
||||
queueSizeUpdater.addAndGet(this, bytes);
|
||||
return ResourceLimits.Outcome.SUCCESS;
|
||||
}
|
||||
|
||||
// we know we don't have enough local queue capacity for the entire message, so we need to borrow some from reserve capacity
|
||||
long allocatedExcess = min(currentQueueSize + bytes - queueCapacity, bytes);
|
||||
|
||||
if (!globalReserve.tryAllocate(allocatedExcess))
|
||||
return ResourceLimits.Outcome.INSUFFICIENT_GLOBAL;
|
||||
|
||||
if (!endpointReserve.tryAllocate(allocatedExcess))
|
||||
{
|
||||
globalReserve.release(allocatedExcess);
|
||||
globalWaitQueue.signal();
|
||||
return ResourceLimits.Outcome.INSUFFICIENT_ENDPOINT;
|
||||
}
|
||||
|
||||
long newQueueSize = queueSizeUpdater.addAndGet(this, bytes);
|
||||
long actualExcess = max(0, min(newQueueSize - queueCapacity, bytes));
|
||||
|
||||
/*
|
||||
* It's possible that some permits were released at some point after we loaded current queueSize,
|
||||
* and we can satisfy more of the permits using our exclusive per-connection capacity, needing
|
||||
* less than previously estimated from the reserves. If that's the case, release the now unneeded
|
||||
* permit excess back to endpoint/global reserves.
|
||||
*/
|
||||
if (actualExcess != allocatedExcess) // actualExcess < allocatedExcess
|
||||
{
|
||||
long excess = allocatedExcess - actualExcess;
|
||||
|
||||
endpointReserve.release(excess);
|
||||
globalReserve.release(excess);
|
||||
|
||||
endpointWaitQueue.signal();
|
||||
globalWaitQueue.signal();
|
||||
}
|
||||
|
||||
return ResourceLimits.Outcome.SUCCESS;
|
||||
}
|
||||
|
||||
private void releaseCapacity(int bytes)
|
||||
{
|
||||
long oldQueueSize = queueSizeUpdater.getAndAdd(this, -bytes);
|
||||
if (oldQueueSize > queueCapacity)
|
||||
{
|
||||
long excess = min(oldQueueSize - queueCapacity, bytes);
|
||||
|
||||
endpointReserveCapacity.release(excess);
|
||||
globalReserveCapacity.release(excess);
|
||||
|
||||
endpointWaitQueue.signal();
|
||||
globalWaitQueue.signal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to release capacity for a message that has been fully, successfully processed.
|
||||
*
|
||||
* Normally no different from invoking {@link #releaseCapacity(int)}, but is necessary for the verifier
|
||||
* to be able to delay capacity release for backpressure testing.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected void releaseProcessedCapacity(int size, Header header)
|
||||
{
|
||||
releaseCapacity(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
|
||||
{
|
||||
try
|
||||
{
|
||||
exceptionCaught(cause);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
logger.error("Unexpected exception in {}.exceptionCaught", this.getClass().getSimpleName(), t);
|
||||
}
|
||||
}
|
||||
|
||||
private void exceptionCaught(Throwable cause)
|
||||
{
|
||||
decoder.discard();
|
||||
|
||||
JVMStabilityInspector.inspectThrowable(cause);
|
||||
|
||||
if (cause instanceof Message.InvalidLegacyProtocolMagic)
|
||||
logger.error("{} invalid, unrecoverable CRC mismatch detected while reading messages - closing the connection", id());
|
||||
else
|
||||
logger.error("{} unexpected exception caught while processing inbound messages; terminating connection", id(), cause);
|
||||
|
||||
channel.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx)
|
||||
{
|
||||
isClosed = true;
|
||||
|
||||
if (null != largeMessage)
|
||||
largeMessage.abort();
|
||||
|
||||
if (null != ticket)
|
||||
ticket.invalidate();
|
||||
|
||||
onClosed.call(this);
|
||||
}
|
||||
|
||||
private EventLoop eventLoop()
|
||||
{
|
||||
return channel.eventLoop();
|
||||
}
|
||||
|
||||
String id(boolean includeReal)
|
||||
{
|
||||
if (!includeReal)
|
||||
|
|
@ -687,7 +264,7 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
type, channel.id().asShortText());
|
||||
}
|
||||
|
||||
String id()
|
||||
protected String id()
|
||||
{
|
||||
return SocketFactory.channelId(peer, self, type, channel.id().asShortText());
|
||||
}
|
||||
|
|
@ -698,6 +275,33 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
return id();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
|
||||
{
|
||||
try
|
||||
{
|
||||
fatalExceptionCaught(cause);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
logger.error("Unexpected exception in {}.exceptionCaught", this.getClass().getSimpleName(), t);
|
||||
}
|
||||
}
|
||||
|
||||
protected void fatalExceptionCaught(Throwable cause)
|
||||
{
|
||||
decoder.discard();
|
||||
|
||||
JVMStabilityInspector.inspectThrowable(cause);
|
||||
|
||||
if (cause instanceof Message.InvalidLegacyProtocolMagic)
|
||||
logger.error("{} invalid, unrecoverable CRC mismatch detected while reading messages - closing the connection", id());
|
||||
else
|
||||
logger.error("{} unexpected exception caught while processing inbound messages; terminating connection", id(), cause);
|
||||
|
||||
channel.close();
|
||||
}
|
||||
|
||||
/*
|
||||
* A large-message frame-accumulating state machine.
|
||||
*
|
||||
|
|
@ -710,28 +314,16 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
* accumulated frames, or in gathering more - so we release the ones we already have, and
|
||||
* skip any remaining ones, alongside with returning memory permits early.
|
||||
*/
|
||||
private class LargeMessage
|
||||
private class LargeMessage extends AbstractMessageHandler.LargeMessage<Header>
|
||||
{
|
||||
private final int size;
|
||||
private final Header header;
|
||||
|
||||
private final List<ShareableBytes> buffers = new ArrayList<>();
|
||||
private int received;
|
||||
|
||||
private boolean isExpired;
|
||||
private boolean isCorrupt;
|
||||
|
||||
private LargeMessage(int size, Header header, boolean isExpired)
|
||||
{
|
||||
this.size = size;
|
||||
this.header = header;
|
||||
this.isExpired = isExpired;
|
||||
super(size, header, header.expiresAtNanos, isExpired);
|
||||
}
|
||||
|
||||
private LargeMessage(int size, Header header, ShareableBytes bytes)
|
||||
{
|
||||
this(size, header, false);
|
||||
buffers.add(bytes);
|
||||
super(size, header, header.expiresAtNanos, bytes);
|
||||
}
|
||||
|
||||
private void schedule()
|
||||
|
|
@ -739,47 +331,7 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
dispatch(new ProcessLargeMessage(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this was the last frame of the large message.
|
||||
*/
|
||||
private boolean supply(Frame frame)
|
||||
{
|
||||
if (frame instanceof IntactFrame)
|
||||
onIntactFrame((IntactFrame) frame);
|
||||
else
|
||||
onCorruptFrame();
|
||||
|
||||
received += frame.frameSize;
|
||||
if (size == received)
|
||||
onComplete();
|
||||
return size == received;
|
||||
}
|
||||
|
||||
private void onIntactFrame(IntactFrame frame)
|
||||
{
|
||||
boolean expires = approxTime.isAfter(header.expiresAtNanos);
|
||||
if (!isExpired && !isCorrupt)
|
||||
{
|
||||
if (!expires)
|
||||
{
|
||||
buffers.add(frame.contents.sliceAndConsume(frame.frameSize).share());
|
||||
return;
|
||||
}
|
||||
releaseBuffersAndCapacity(); // release resources once we transition from normal state to expired
|
||||
}
|
||||
frame.consume();
|
||||
isExpired |= expires;
|
||||
}
|
||||
|
||||
private void onCorruptFrame()
|
||||
{
|
||||
if (!isExpired && !isCorrupt)
|
||||
releaseBuffersAndCapacity(); // release resources once we transition from normal state to corrupt
|
||||
isCorrupt = true;
|
||||
isExpired |= approxTime.isAfter(header.expiresAtNanos);
|
||||
}
|
||||
|
||||
private void onComplete()
|
||||
protected void onComplete()
|
||||
{
|
||||
long timeElapsed = approxTime.now() - header.createdAtNanos;
|
||||
|
||||
|
|
@ -798,23 +350,13 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
}
|
||||
}
|
||||
|
||||
private void abort()
|
||||
protected void abort()
|
||||
{
|
||||
if (!isExpired && !isCorrupt)
|
||||
releaseBuffersAndCapacity(); // release resources if in normal state when abort() is invoked
|
||||
callbacks.onClosedBeforeArrival(size, header, received, isCorrupt, isExpired);
|
||||
}
|
||||
|
||||
private void releaseBuffers()
|
||||
{
|
||||
buffers.forEach(ShareableBytes::release); buffers.clear();
|
||||
}
|
||||
|
||||
private void releaseBuffersAndCapacity()
|
||||
{
|
||||
releaseBuffers(); releaseCapacity(size);
|
||||
}
|
||||
|
||||
private Message deserialize()
|
||||
{
|
||||
try (ChunkedInputPlus input = ChunkedInputPlus.of(buffers))
|
||||
|
|
@ -968,232 +510,4 @@ public class InboundMessageHandler extends ChannelInboundHandlerAdapter implemen
|
|||
message.releaseBuffers(); // releases buffers if they haven't been yet (by deserialize() call)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A special-purpose wait queue to park inbound message handlers that failed to allocate
|
||||
* reserve capacity for a message in. Upon such failure a handler registers itself with
|
||||
* a {@link WaitQueue} of the appropriate kind (either ENDPOINT or GLOBAL - if failed
|
||||
* to allocate endpoint or global reserve capacity, respectively), stops processing any
|
||||
* accumulated frames or receiving new ones, and waits - until reactivated.
|
||||
*
|
||||
* Every time permits are returned to an endpoint or global {@link Limit}, the respective
|
||||
* queue gets signalled, and if there are any handlers registered in it, we will attempt
|
||||
* to reactivate as many waiting handlers as current available reserve capacity allows
|
||||
* us to - immediately, on the {@link #signal()}-calling thread. At most one such attempt
|
||||
* will be in progress at any given time.
|
||||
*
|
||||
* Handlers that can be reactivated will be grouped by their {@link EventLoop} and a single
|
||||
* {@link ReactivateHandlers} task will be scheduled per event loop, on the corresponding
|
||||
* event loops.
|
||||
*
|
||||
* When run, the {@link ReactivateHandlers} task will ask each handler in its group to first
|
||||
* process one message - using preallocated reserve capacity - and if no obstacles were met -
|
||||
* reactivate the handlers, this time using their regular reserves.
|
||||
*
|
||||
* See {@link WaitQueue#schedule()}, {@link ReactivateHandlers#run()}, {@link Ticket#reactivateHandler(Limit)}.
|
||||
*/
|
||||
public static final class WaitQueue
|
||||
{
|
||||
enum Kind { ENDPOINT, GLOBAL }
|
||||
|
||||
private static final int NOT_RUNNING = 0;
|
||||
@SuppressWarnings("unused")
|
||||
private static final int RUNNING = 1;
|
||||
private static final int RUN_AGAIN = 2;
|
||||
|
||||
private volatile int scheduled;
|
||||
private static final AtomicIntegerFieldUpdater<WaitQueue> scheduledUpdater =
|
||||
AtomicIntegerFieldUpdater.newUpdater(WaitQueue.class, "scheduled");
|
||||
|
||||
private final Kind kind;
|
||||
private final Limit reserveCapacity;
|
||||
|
||||
private final ManyToOneConcurrentLinkedQueue<Ticket> queue = new ManyToOneConcurrentLinkedQueue<>();
|
||||
|
||||
private WaitQueue(Kind kind, Limit reserveCapacity)
|
||||
{
|
||||
this.kind = kind;
|
||||
this.reserveCapacity = reserveCapacity;
|
||||
}
|
||||
|
||||
public static WaitQueue endpoint(Limit endpointReserveCapacity)
|
||||
{
|
||||
return new WaitQueue(Kind.ENDPOINT, endpointReserveCapacity);
|
||||
}
|
||||
|
||||
public static WaitQueue global(Limit globalReserveCapacity)
|
||||
{
|
||||
return new WaitQueue(Kind.GLOBAL, globalReserveCapacity);
|
||||
}
|
||||
|
||||
private Ticket register(InboundMessageHandler handler, int bytesRequested, long registeredAtNanos, long expiresAtNanos)
|
||||
{
|
||||
Ticket ticket = new Ticket(this, handler, bytesRequested, registeredAtNanos, expiresAtNanos);
|
||||
Ticket previous = queue.relaxedPeekLastAndOffer(ticket);
|
||||
if (null == previous || !previous.isWaiting())
|
||||
signal(); // only signal the queue if this handler is first to register
|
||||
return ticket;
|
||||
}
|
||||
|
||||
private void signal()
|
||||
{
|
||||
if (queue.relaxedIsEmpty())
|
||||
return; // we can return early if no handlers have registered with the wait queue
|
||||
|
||||
if (NOT_RUNNING == scheduledUpdater.getAndUpdate(this, i -> min(RUN_AGAIN, i + 1)))
|
||||
{
|
||||
do
|
||||
{
|
||||
schedule();
|
||||
}
|
||||
while (RUN_AGAIN == scheduledUpdater.getAndDecrement(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void schedule()
|
||||
{
|
||||
Map<EventLoop, ReactivateHandlers> tasks = null;
|
||||
|
||||
long currentTimeNanos = approxTime.now();
|
||||
|
||||
Ticket t;
|
||||
while ((t = queue.peek()) != null)
|
||||
{
|
||||
if (!t.call()) // invalidated
|
||||
{
|
||||
queue.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isLive = t.isLive(currentTimeNanos);
|
||||
if (isLive && !reserveCapacity.tryAllocate(t.bytesRequested))
|
||||
{
|
||||
if (!t.reset()) // the ticket was invalidated after being called but before now
|
||||
{
|
||||
queue.remove();
|
||||
continue;
|
||||
}
|
||||
break; // TODO: traverse the entire queue to unblock handlers that have expired or invalidated tickets
|
||||
}
|
||||
|
||||
if (null == tasks)
|
||||
tasks = new IdentityHashMap<>();
|
||||
|
||||
queue.remove();
|
||||
tasks.computeIfAbsent(t.handler.eventLoop(), e -> new ReactivateHandlers()).add(t, isLive);
|
||||
}
|
||||
|
||||
if (null != tasks)
|
||||
tasks.forEach(EventLoop::execute);
|
||||
}
|
||||
|
||||
private class ReactivateHandlers implements Runnable
|
||||
{
|
||||
List<Ticket> tickets = new ArrayList<>();
|
||||
long capacity = 0L;
|
||||
|
||||
private void add(Ticket ticket, boolean isLive)
|
||||
{
|
||||
tickets.add(ticket);
|
||||
if (isLive) capacity += ticket.bytesRequested;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
Limit limit = new ResourceLimits.Basic(capacity);
|
||||
try
|
||||
{
|
||||
for (Ticket ticket : tickets)
|
||||
ticket.reactivateHandler(limit);
|
||||
}
|
||||
finally
|
||||
{
|
||||
/*
|
||||
* Free up any unused capacity, if any. Will be non-zero if one or more handlers were closed
|
||||
* when we attempted to run their callback, or used more of their other reserve; or if the first
|
||||
* message in the unprocessed stream has expired in the narrow time window.
|
||||
*/
|
||||
long remaining = limit.remaining();
|
||||
if (remaining > 0)
|
||||
{
|
||||
reserveCapacity.release(remaining);
|
||||
signal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Ticket
|
||||
{
|
||||
private static final int WAITING = 0;
|
||||
private static final int CALLED = 1;
|
||||
private static final int INVALIDATED = 2; // invalidated by a handler that got closed
|
||||
|
||||
private volatile int state;
|
||||
private static final AtomicIntegerFieldUpdater<Ticket> stateUpdater =
|
||||
AtomicIntegerFieldUpdater.newUpdater(Ticket.class, "state");
|
||||
|
||||
private final WaitQueue waitQueue;
|
||||
private final InboundMessageHandler handler;
|
||||
private final int bytesRequested;
|
||||
private final long reigsteredAtNanos;
|
||||
private final long expiresAtNanos;
|
||||
|
||||
private Ticket(WaitQueue waitQueue, InboundMessageHandler handler, int bytesRequested, long registeredAtNanos, long expiresAtNanos)
|
||||
{
|
||||
this.waitQueue = waitQueue;
|
||||
this.handler = handler;
|
||||
this.bytesRequested = bytesRequested;
|
||||
this.reigsteredAtNanos = registeredAtNanos;
|
||||
this.expiresAtNanos = expiresAtNanos;
|
||||
}
|
||||
|
||||
private void reactivateHandler(Limit capacity)
|
||||
{
|
||||
long elapsedNanos = approxTime.now() - reigsteredAtNanos;
|
||||
try
|
||||
{
|
||||
if (waitQueue.kind == Kind.ENDPOINT)
|
||||
handler.onEndpointReserveCapacityRegained(capacity, elapsedNanos);
|
||||
else
|
||||
handler.onGlobalReserveCapacityRegained(capacity, elapsedNanos);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
logger.error("{} exception caught while reactivating a handler", handler.id(), t);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWaiting()
|
||||
{
|
||||
return state == WAITING;
|
||||
}
|
||||
|
||||
private boolean isLive(long currentTimeNanos)
|
||||
{
|
||||
return !approxTime.isAfter(currentTimeNanos, expiresAtNanos);
|
||||
}
|
||||
|
||||
private void invalidate()
|
||||
{
|
||||
state = INVALIDATED;
|
||||
waitQueue.signal();
|
||||
}
|
||||
|
||||
private boolean call()
|
||||
{
|
||||
return stateUpdater.compareAndSet(this, WAITING, CALLED);
|
||||
}
|
||||
|
||||
private boolean reset()
|
||||
{
|
||||
return stateUpdater.compareAndSet(this, CALLED, WAITING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnHandlerClosed
|
||||
{
|
||||
void call(InboundMessageHandler handler);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import io.netty.channel.Channel;
|
|||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.metrics.InternodeInboundMetrics;
|
||||
import org.apache.cassandra.net.Message.Header;
|
||||
import org.apache.cassandra.utils.ApproximateTime;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.NANOSECONDS;
|
||||
import static org.apache.cassandra.utils.MonotonicClock.approxTime;
|
||||
|
|
@ -161,10 +160,11 @@ public final class InboundMessageHandlers
|
|||
metrics.release();
|
||||
}
|
||||
|
||||
private void onHandlerClosed(InboundMessageHandler handler)
|
||||
private void onHandlerClosed(AbstractMessageHandler handler)
|
||||
{
|
||||
assert handler instanceof InboundMessageHandler;
|
||||
handlers.remove(handler);
|
||||
absorbCounters(handler);
|
||||
absorbCounters((InboundMessageHandler)handler);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -163,12 +163,12 @@ public abstract class ResourceLimits
|
|||
/**
|
||||
* A cheaper, thread-unsafe permit container to be used for unshared limits.
|
||||
*/
|
||||
static class Basic implements Limit
|
||||
public static class Basic implements Limit
|
||||
{
|
||||
private long limit;
|
||||
private long using;
|
||||
|
||||
Basic(long limit)
|
||||
public Basic(long limit)
|
||||
{
|
||||
this.limit = limit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import org.apache.cassandra.utils.memory.BufferPools;
|
|||
* When sharing is necessary, {@link #share()} method must be invoked by the owning thread
|
||||
* before a {@link ShareableBytes} instance can be shared with another thread.
|
||||
*/
|
||||
class ShareableBytes
|
||||
public class ShareableBytes
|
||||
{
|
||||
private final ByteBuffer bytes;
|
||||
private final ShareableBytes owner;
|
||||
|
|
@ -53,18 +53,18 @@ class ShareableBytes
|
|||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
ByteBuffer get()
|
||||
public ByteBuffer get()
|
||||
{
|
||||
assert owner.count != 0;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
boolean hasRemaining()
|
||||
public boolean hasRemaining()
|
||||
{
|
||||
return bytes.hasRemaining();
|
||||
}
|
||||
|
||||
int remaining()
|
||||
public int remaining()
|
||||
{
|
||||
return bytes.remaining();
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ class ShareableBytes
|
|||
* The first invocation must occur while the calling thread has exclusive access (though there may be more
|
||||
* than one 'owner', these must all either be owned by the calling thread or otherwise not being used)
|
||||
*/
|
||||
ShareableBytes share()
|
||||
public ShareableBytes share()
|
||||
{
|
||||
int count = owner.count;
|
||||
if (count < 0)
|
||||
|
|
@ -119,7 +119,7 @@ class ShareableBytes
|
|||
}
|
||||
}
|
||||
|
||||
void release()
|
||||
public void release()
|
||||
{
|
||||
owner.doRelease();
|
||||
}
|
||||
|
|
@ -147,7 +147,7 @@ class ShareableBytes
|
|||
/**
|
||||
* Create a slice over the next {@code length} bytes, consuming them from our buffer, and incrementing the owner count
|
||||
*/
|
||||
ShareableBytes sliceAndConsume(int length)
|
||||
public ShareableBytes sliceAndConsume(int length)
|
||||
{
|
||||
int begin = bytes.position();
|
||||
int end = begin + length;
|
||||
|
|
@ -166,7 +166,7 @@ class ShareableBytes
|
|||
return new ShareableBytes(owner.retain(), slice);
|
||||
}
|
||||
|
||||
static ShareableBytes wrap(ByteBuffer buffer)
|
||||
public static ShareableBytes wrap(ByteBuffer buffer)
|
||||
{
|
||||
return new ShareableBytes(buffer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import io.netty.util.Version;
|
|||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.metrics.ClientMetrics;
|
||||
import org.apache.cassandra.transport.Message;
|
||||
import org.apache.cassandra.transport.Dispatcher;
|
||||
import org.apache.cassandra.transport.Server;
|
||||
import org.apache.cassandra.utils.NativeLibrary;
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ public class NativeTransportService
|
|||
// shutdown executors used by netty for native transport server
|
||||
workerGroup.shutdownGracefully(3, 5, TimeUnit.SECONDS).awaitUninterruptibly();
|
||||
|
||||
Message.Dispatcher.shutdown();
|
||||
Dispatcher.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ import org.apache.cassandra.schema.TableMetadataRef;
|
|||
import org.apache.cassandra.schema.ViewMetadata;
|
||||
import org.apache.cassandra.streaming.*;
|
||||
import org.apache.cassandra.tracing.TraceKeyspace;
|
||||
import org.apache.cassandra.transport.ClientResourceLimits;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
import org.apache.cassandra.transport.Server;
|
||||
import org.apache.cassandra.utils.*;
|
||||
|
|
@ -5682,25 +5683,25 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
|
|||
@Override
|
||||
public long getNativeTransportMaxConcurrentRequestsInBytes()
|
||||
{
|
||||
return Server.EndpointPayloadTracker.getGlobalLimit();
|
||||
return ClientResourceLimits.getGlobalLimit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNativeTransportMaxConcurrentRequestsInBytes(long newLimit)
|
||||
{
|
||||
Server.EndpointPayloadTracker.setGlobalLimit(newLimit);
|
||||
ClientResourceLimits.setGlobalLimit(newLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNativeTransportMaxConcurrentRequestsInBytesPerIp()
|
||||
{
|
||||
return Server.EndpointPayloadTracker.getEndpointLimit();
|
||||
return ClientResourceLimits.getEndpointLimit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNativeTransportMaxConcurrentRequestsInBytesPerIp(long newLimit)
|
||||
{
|
||||
Server.EndpointPayloadTracker.setEndpointLimit(newLimit);
|
||||
ClientResourceLimits.setEndpointLimit(newLimit);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
|
|||
|
|
@ -600,10 +600,4 @@ public abstract class CBUtil
|
|||
return bytes;
|
||||
}
|
||||
|
||||
public static int readUnsignedShort(ByteBuf buf)
|
||||
{
|
||||
int ch1 = buf.readByte() & 0xFF;
|
||||
int ch2 = buf.readByte() & 0xFF;
|
||||
return (ch1 << 8) + (ch2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,484 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import org.apache.cassandra.exceptions.OverloadedException;
|
||||
import org.apache.cassandra.metrics.ClientMetrics;
|
||||
import org.apache.cassandra.metrics.ClientMessageSizeMetrics;
|
||||
import org.apache.cassandra.net.AbstractMessageHandler;
|
||||
import org.apache.cassandra.net.FrameDecoder;
|
||||
import org.apache.cassandra.net.FrameDecoder.IntactFrame;
|
||||
import org.apache.cassandra.net.FrameEncoder;
|
||||
import org.apache.cassandra.net.ResourceLimits;
|
||||
import org.apache.cassandra.net.ResourceLimits.Limit;
|
||||
import org.apache.cassandra.net.ShareableBytes;
|
||||
import org.apache.cassandra.transport.Flusher.FlushItem.Framed;
|
||||
import org.apache.cassandra.transport.messages.ErrorMessage;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
|
||||
import static org.apache.cassandra.utils.MonotonicClock.approxTime;
|
||||
|
||||
/**
|
||||
* Implementation of {@link AbstractMessageHandler} for processing CQL messages which comprise a {@link Message} wrapped
|
||||
* in an {@link Envelope}. This class is parameterized by a {@link Message} subtype, expected to be either
|
||||
* {@link Message.Request} or {@link Message.Response}. Most commonly, an instance for handling {@link Message.Request}
|
||||
* is created for each inbound CQL client connection.
|
||||
*
|
||||
* # Small vs large messages
|
||||
* Small messages are deserialized in place, and then handed off to a consumer for processing.
|
||||
* Large messages accumulate frames until all bytes for the envelope are received, then concatenate and deserialize the
|
||||
* frames on the event loop thread and pass them on to the same consumer.
|
||||
*
|
||||
* # Flow control (backpressure)
|
||||
*
|
||||
* The size of an incoming message is explicit in the {@link Envelope.Header}.
|
||||
* {@link org.apache.cassandra.net.Message.Serializer#inferMessageSize(ByteBuffer, int, int, int)}.
|
||||
*
|
||||
* By default, every connection has 1MiB of exlusive permits available before needing to access the per-endpoint
|
||||
* and global reserves. By default, those reserves are sized proportionally to the heap - 2.5% of heap per-endpoint
|
||||
* and a 10% for the global reserve.
|
||||
*
|
||||
* Permits are held while CQL messages are processed and released after the response has been encoded into the
|
||||
* buffers of the response frame.
|
||||
*/
|
||||
public class CQLMessageHandler<M extends Message> extends AbstractMessageHandler
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(CQLMessageHandler.class);
|
||||
private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1L, TimeUnit.SECONDS);
|
||||
|
||||
public static final int LARGE_MESSAGE_THRESHOLD = FrameEncoder.Payload.MAX_SIZE - 1;
|
||||
|
||||
private final Envelope.Decoder envelopeDecoder;
|
||||
private final Message.Decoder<M> messageDecoder;
|
||||
private final FrameEncoder.PayloadAllocator payloadAllocator;
|
||||
private final MessageConsumer<M> dispatcher;
|
||||
private final ErrorHandler errorHandler;
|
||||
private final boolean throwOnOverload;
|
||||
|
||||
long channelPayloadBytesInFlight;
|
||||
|
||||
interface MessageConsumer<M extends Message>
|
||||
{
|
||||
void accept(Channel channel, M message, Dispatcher.FlushItemConverter toFlushItem);
|
||||
}
|
||||
|
||||
interface ErrorHandler
|
||||
{
|
||||
void accept(Throwable error);
|
||||
}
|
||||
|
||||
CQLMessageHandler(Channel channel,
|
||||
FrameDecoder decoder,
|
||||
Envelope.Decoder envelopeDecoder,
|
||||
Message.Decoder<M> messageDecoder,
|
||||
MessageConsumer<M> dispatcher,
|
||||
FrameEncoder.PayloadAllocator payloadAllocator,
|
||||
int queueCapacity,
|
||||
ClientResourceLimits.ResourceProvider resources,
|
||||
OnHandlerClosed onClosed,
|
||||
ErrorHandler errorHandler,
|
||||
boolean throwOnOverload)
|
||||
{
|
||||
super(decoder,
|
||||
channel,
|
||||
LARGE_MESSAGE_THRESHOLD,
|
||||
queueCapacity,
|
||||
resources.endpointLimit(),
|
||||
resources.globalLimit(),
|
||||
resources.endpointWaitQueue(),
|
||||
resources.globalWaitQueue(),
|
||||
onClosed);
|
||||
this.envelopeDecoder = envelopeDecoder;
|
||||
this.messageDecoder = messageDecoder;
|
||||
this.payloadAllocator = payloadAllocator;
|
||||
this.dispatcher = dispatcher;
|
||||
this.errorHandler = errorHandler;
|
||||
this.throwOnOverload = throwOnOverload;
|
||||
}
|
||||
|
||||
protected boolean processOneContainedMessage(ShareableBytes bytes, Limit endpointReserve, Limit globalReserve)
|
||||
{
|
||||
Envelope.Header header = extractHeader(bytes);
|
||||
// null indicates a failure to extract the CQL message header.
|
||||
// This will trigger a protocol exception and closing of the connection.
|
||||
if (null == header)
|
||||
return false;
|
||||
|
||||
// max CQL message size defaults to 256mb, so should be safe to downcast
|
||||
int messageSize = Ints.checkedCast(header.bodySizeInBytes);
|
||||
|
||||
if (throwOnOverload)
|
||||
{
|
||||
if (!acquireCapacity(header, endpointReserve, globalReserve))
|
||||
{
|
||||
// discard the request and throw an exception
|
||||
ClientMetrics.instance.markRequestDiscarded();
|
||||
logger.trace("Discarded request of size: {}. InflightChannelRequestPayload: {}, " +
|
||||
"InflightEndpointRequestPayload: {}, InflightOverallRequestPayload: {}, Header: {}",
|
||||
messageSize,
|
||||
channelPayloadBytesInFlight,
|
||||
endpointReserve.using(),
|
||||
globalReserve.using(),
|
||||
header);
|
||||
|
||||
handleError(new OverloadedException("Server is in overloaded state. " +
|
||||
"Cannot accept more requests at this point"), header);
|
||||
|
||||
// Don't stop processing incoming messages, rely on the client to apply
|
||||
// backpressure when it receives OverloadedException
|
||||
// but discard this message as we're responding with the overloaded error
|
||||
incrementReceivedMessageMetrics(messageSize);
|
||||
ByteBuffer buf = bytes.get();
|
||||
buf.position(buf.position() + Envelope.Header.LENGTH + messageSize);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (!acquireCapacityAndQueueOnFailure(header, endpointReserve, globalReserve))
|
||||
{
|
||||
// set backpressure on the channel, queuing the request until we have capacity
|
||||
ClientMetrics.instance.pauseConnection();
|
||||
return false;
|
||||
}
|
||||
|
||||
channelPayloadBytesInFlight += messageSize;
|
||||
incrementReceivedMessageMetrics(messageSize);
|
||||
processRequest(composeRequest(header, bytes));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void incrementReceivedMessageMetrics(int messageSize)
|
||||
{
|
||||
receivedCount++;
|
||||
receivedBytes += messageSize + Envelope.Header.LENGTH;
|
||||
ClientMessageSizeMetrics.bytesReceived.inc(messageSize + Envelope.Header.LENGTH);
|
||||
ClientMessageSizeMetrics.bytesReceivedPerRequest.update(messageSize + Envelope.Header.LENGTH);
|
||||
}
|
||||
|
||||
private Envelope.Header extractHeader(ShareableBytes bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
return envelopeDecoder.extractHeader(bytes.get());
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
JVMStabilityInspector.inspectThrowable(t);
|
||||
logger.error("{} unexpected exception caught while deserializing a message header", id(), t);
|
||||
// Ideally we would recover from this as we could simply drop the entire frame.
|
||||
// However, we have no good way to communicate this back to the client as we don't
|
||||
// yet know the stream id. So this will trigger an Error response and the connection
|
||||
// to be closed.
|
||||
handleError(new ProtocolException("Deserialization error processing CQL payload"));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Envelope composeRequest(Envelope.Header header, ShareableBytes bytes)
|
||||
{
|
||||
// extract body
|
||||
ByteBuffer buf = bytes.get();
|
||||
int idx = buf.position() + Envelope.Header.LENGTH;
|
||||
final int end = idx + Ints.checkedCast(header.bodySizeInBytes);
|
||||
ByteBuf body = Unpooled.wrappedBuffer(buf.slice());
|
||||
body.readerIndex(Envelope.Header.LENGTH);
|
||||
body.retain();
|
||||
buf.position(end);
|
||||
return new Envelope(header, body);
|
||||
}
|
||||
|
||||
protected void processRequest(Envelope request)
|
||||
{
|
||||
M message = null;
|
||||
try
|
||||
{
|
||||
message = messageDecoder.decode(channel, request);
|
||||
dispatcher.accept(channel, message, this::toFlushItem);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (message != null)
|
||||
request.release();
|
||||
handleErrorAndRelease(e, request.header);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For "expected" errors this ensures we pass a WrappedException,
|
||||
* which contains a streamId, to the error handler. This makes
|
||||
* sure that whereever possible, the streamId is propagated back
|
||||
* to the client.
|
||||
* This also releases the capacity acquired for processing as
|
||||
* indicated by supplied header.
|
||||
* @param t
|
||||
* @param header
|
||||
*/
|
||||
private void handleErrorAndRelease(Throwable t, Envelope.Header header)
|
||||
{
|
||||
release(header);
|
||||
handleError(t, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* For "expected" errors this ensures we pass a WrappedException,
|
||||
* which contains a streamId, to the error handler. This makes
|
||||
* sure that whereever possible, the streamId is propagated back
|
||||
* to the client.
|
||||
* This variant doesn't call release as it is intended for use
|
||||
* when an error occurs without any capacity being acquired.
|
||||
* Typically, this would be the result of an acquisition failure
|
||||
* if the THROW_ON_OVERLOAD option has been specified by the client.
|
||||
* @param t
|
||||
* @param header
|
||||
*/
|
||||
private void handleError(Throwable t, Envelope.Header header)
|
||||
{
|
||||
errorHandler.accept(ErrorMessage.wrap(t, header.streamId));
|
||||
}
|
||||
|
||||
/**
|
||||
* For use in the case where the error can't be mapped to a specific stream id,
|
||||
* such as a corrupted frame, or when extracting a CQL message from the frame's
|
||||
* payload fails. This does not attempt to release any resources, as these errors
|
||||
* should only occur before any capacity acquisition is attempted (e.g. on receipt
|
||||
* of a corrupt frame, or failure to extract a CQL message from the envelope).
|
||||
*
|
||||
* @param t
|
||||
*/
|
||||
private void handleError(Throwable t)
|
||||
{
|
||||
errorHandler.accept(t);
|
||||
}
|
||||
|
||||
// Acts as a Dispatcher.FlushItemConverter
|
||||
private Framed toFlushItem(Channel channel, Message.Request request, Message.Response response)
|
||||
{
|
||||
// Returns a FlushItem.Framed instance which wraps a Consumer<FlushItem> that performs
|
||||
// the work of returning the capacity allocated for processing the request.
|
||||
// The Dispatcher will call this to obtain the FlushItem to enqueue with its Flusher once
|
||||
// a dispatched request has been processed.
|
||||
|
||||
Envelope responseFrame = response.encode(request.getSource().header.version);
|
||||
int responseSize = envelopeSize(responseFrame.header);
|
||||
ClientMessageSizeMetrics.bytesSent.inc(responseSize);
|
||||
ClientMessageSizeMetrics.bytesSentPerResponse.update(responseSize);
|
||||
|
||||
return new Framed(channel,
|
||||
responseFrame,
|
||||
request.getSource(),
|
||||
payloadAllocator,
|
||||
this::release);
|
||||
}
|
||||
|
||||
private void release(Flusher.FlushItem<Envelope> flushItem)
|
||||
{
|
||||
release(flushItem.request.header);
|
||||
flushItem.request.release();
|
||||
flushItem.response.release();
|
||||
}
|
||||
|
||||
private void release(Envelope.Header header)
|
||||
{
|
||||
releaseCapacity(Ints.checkedCast(header.bodySizeInBytes));
|
||||
channelPayloadBytesInFlight -= header.bodySizeInBytes;
|
||||
}
|
||||
|
||||
/*
|
||||
* Handling of multi-frame large messages
|
||||
*/
|
||||
protected boolean processFirstFrameOfLargeMessage(IntactFrame frame, Limit endpointReserve, Limit globalReserve) throws IOException
|
||||
{
|
||||
ShareableBytes bytes = frame.contents;
|
||||
ByteBuffer buf = bytes.get();
|
||||
try
|
||||
{
|
||||
Envelope.Header header = envelopeDecoder.extractHeader(buf);
|
||||
// max CQL message size defaults to 256mb, so should be safe to downcast
|
||||
int messageSize = Ints.checkedCast(header.bodySizeInBytes);
|
||||
receivedBytes += buf.remaining();
|
||||
|
||||
if (throwOnOverload)
|
||||
{
|
||||
LargeMessage largeMessage = new LargeMessage(header);
|
||||
if (!acquireCapacity(header, endpointReserve, globalReserve))
|
||||
{
|
||||
// discard the request and throw an exception
|
||||
ClientMetrics.instance.markRequestDiscarded();
|
||||
logger.trace("Discarded request of size: {}. InflightChannelRequestPayload: {}, " +
|
||||
"InflightEndpointRequestPayload: {}, InflightOverallRequestPayload: {}, Header: {}",
|
||||
messageSize,
|
||||
channelPayloadBytesInFlight,
|
||||
endpointReserve.using(),
|
||||
globalReserve.using(),
|
||||
header);
|
||||
|
||||
// mark as overloaded so that we consume
|
||||
// subsequent frames and then discard the message
|
||||
largeMessage.markOverloaded();
|
||||
}
|
||||
this.largeMessage = largeMessage;
|
||||
largeMessage.supply(frame);
|
||||
// Don't stop processing incoming frames, rely on the client to apply
|
||||
// backpressure when it receives OverloadedException
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!acquireCapacityAndQueueOnFailure(header, endpointReserve, globalReserve))
|
||||
{
|
||||
receivedBytes += frame.frameSize;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
largeMessage = new LargeMessage(header);
|
||||
largeMessage.supply(frame);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new IOException("Error decoding CQL Message", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String id()
|
||||
{
|
||||
return channel.id().asShortText();
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private boolean acquireCapacityAndQueueOnFailure(Envelope.Header header, Limit endpointReserve, Limit globalReserve)
|
||||
{
|
||||
int bytesRequired = Ints.checkedCast(header.bodySizeInBytes);
|
||||
long currentTimeNanos = approxTime.now();
|
||||
return acquireCapacity(endpointReserve, globalReserve, bytesRequired, currentTimeNanos, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private boolean acquireCapacity(Envelope.Header header, Limit endpointReserve, Limit globalReserve)
|
||||
{
|
||||
int bytesRequired = Ints.checkedCast(header.bodySizeInBytes);
|
||||
return acquireCapacity(endpointReserve, globalReserve, bytesRequired) == ResourceLimits.Outcome.SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* Although it would be possible to recover when certain types of corrupt frame are encountered,
|
||||
* this could cause problems for clients as the payload may contain CQL messages from multiple
|
||||
* streams. Simply dropping the corrupt frame or returning an error response would not give the
|
||||
* client enough information to map back to inflight requests, leading to timeouts.
|
||||
* Instead, we need to fail fast, possibly dropping the connection whenever a corrupt frame is
|
||||
* encountered. Consequently, we terminate the connection (via a ProtocolException) whenever a
|
||||
* corrupt frame is encountered, regardless of its type.
|
||||
*/
|
||||
protected void processCorruptFrame(FrameDecoder.CorruptFrame frame)
|
||||
{
|
||||
corruptFramesUnrecovered++;
|
||||
String error = String.format("%s invalid, unrecoverable CRC mismatch detected in frame %s. Read %d, Computed %d",
|
||||
id(), frame.isRecoverable() ? "body" : "header", frame.readCRC, frame.computedCRC);
|
||||
|
||||
noSpamLogger.error(error);
|
||||
|
||||
// If this is part of a multi-frame message, process it before passing control to the error handler.
|
||||
// This is so we can take care of any housekeeping associated with large messages.
|
||||
if (!frame.isSelfContained)
|
||||
{
|
||||
if (null == largeMessage) // first frame of a large message
|
||||
receivedBytes += frame.frameSize;
|
||||
else // subsequent frame of a large message
|
||||
processSubsequentFrameOfLargeMessage(frame);
|
||||
}
|
||||
|
||||
handleError(new ProtocolException(error));
|
||||
}
|
||||
|
||||
protected void fatalExceptionCaught(Throwable cause)
|
||||
{
|
||||
decoder.discard();
|
||||
logger.warn("Unrecoverable exception caught in CQL message processing pipeline, closing the connection", cause);
|
||||
channel.close();
|
||||
}
|
||||
|
||||
static int envelopeSize(Envelope.Header header)
|
||||
{
|
||||
return Envelope.Header.LENGTH + Ints.checkedCast(header.bodySizeInBytes);
|
||||
}
|
||||
|
||||
private class LargeMessage extends AbstractMessageHandler.LargeMessage<Envelope.Header>
|
||||
{
|
||||
private static final long EXPIRES_AT = Long.MAX_VALUE;
|
||||
|
||||
private boolean overloaded = false;
|
||||
|
||||
private LargeMessage(Envelope.Header header)
|
||||
{
|
||||
super(envelopeSize(header), header, EXPIRES_AT, false);
|
||||
}
|
||||
|
||||
private Envelope assembleFrame()
|
||||
{
|
||||
ByteBuf body = Unpooled.wrappedBuffer(buffers.stream()
|
||||
.map(ShareableBytes::get)
|
||||
.toArray(ByteBuffer[]::new));
|
||||
|
||||
body.readerIndex(Envelope.Header.LENGTH);
|
||||
body.retain();
|
||||
return new Envelope(header, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to indicate that a message should be dropped and not processed.
|
||||
* We do this on receipt of the first frame of a large message if sufficient capacity
|
||||
* cannot be acquired to process it and throwOnOverload is set for the connection.
|
||||
* In this case, the client has elected to shed load rather than apply backpressure
|
||||
* so we must ensure that subsequent frames are consumed from the channel. At that
|
||||
* point an error response is returned to the client, rather than processing the message.
|
||||
*/
|
||||
private void markOverloaded()
|
||||
{
|
||||
overloaded = true;
|
||||
}
|
||||
|
||||
protected void onComplete()
|
||||
{
|
||||
if (overloaded)
|
||||
handleErrorAndRelease(new OverloadedException("Server is in overloaded state. " +
|
||||
"Cannot accept more requests at this point"), header);
|
||||
else if (!isCorrupt)
|
||||
processRequest(assembleFrame());
|
||||
|
||||
}
|
||||
|
||||
protected void abort()
|
||||
{
|
||||
if (!isCorrupt)
|
||||
releaseBuffersAndCapacity(); // release resources if in normal state when abort() is invoked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,13 +33,7 @@ import org.apache.cassandra.cql3.QueryOptions;
|
|||
import org.apache.cassandra.db.ConsistencyLevel;
|
||||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.transport.frame.checksum.ChecksummingTransformer;
|
||||
import org.apache.cassandra.transport.frame.compress.CompressingTransformer;
|
||||
import org.apache.cassandra.transport.frame.compress.Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.LZ4Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.SnappyCompressor;
|
||||
import org.apache.cassandra.transport.messages.*;
|
||||
import org.apache.cassandra.utils.ChecksumType;
|
||||
import org.apache.cassandra.utils.Hex;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
import org.apache.cassandra.utils.MD5Digest;
|
||||
|
|
@ -111,56 +105,25 @@ public class Client extends SimpleClient
|
|||
{
|
||||
Map<String, String> options = new HashMap<String, String>();
|
||||
options.put(StartupMessage.CQL_VERSION, "3.0.0");
|
||||
Compressor compressor = null;
|
||||
ChecksumType checksumType = null;
|
||||
while (iter.hasNext())
|
||||
{
|
||||
String next = iter.next().toLowerCase();
|
||||
switch (next)
|
||||
String next = iter.next();
|
||||
if (next.toLowerCase().equals("snappy"))
|
||||
{
|
||||
case "snappy": {
|
||||
if (options.containsKey(StartupMessage.COMPRESSION))
|
||||
throw new RuntimeException("Multiple compression types supplied");
|
||||
options.put(StartupMessage.COMPRESSION, "snappy");
|
||||
compressor = SnappyCompressor.INSTANCE;
|
||||
break;
|
||||
}
|
||||
case "lz4": {
|
||||
if (options.containsKey(StartupMessage.COMPRESSION))
|
||||
throw new RuntimeException("Multiple compression types supplied");
|
||||
options.put(StartupMessage.COMPRESSION, "lz4");
|
||||
compressor = LZ4Compressor.INSTANCE;
|
||||
break;
|
||||
}
|
||||
case "crc32": {
|
||||
if (options.containsKey(StartupMessage.CHECKSUM))
|
||||
throw new RuntimeException("Multiple checksum types supplied");
|
||||
options.put(StartupMessage.CHECKSUM, ChecksumType.CRC32.name());
|
||||
checksumType = ChecksumType.CRC32;
|
||||
break;
|
||||
}
|
||||
case "adler32": {
|
||||
if (options.containsKey(StartupMessage.CHECKSUM))
|
||||
throw new RuntimeException("Multiple checksum types supplied");
|
||||
options.put(StartupMessage.CHECKSUM, ChecksumType.ADLER32.name());
|
||||
checksumType = ChecksumType.ADLER32;
|
||||
break;
|
||||
}
|
||||
options.put(StartupMessage.COMPRESSION, "snappy");
|
||||
connection.setCompressor(Compressor.SnappyCompressor.instance);
|
||||
}
|
||||
if (next.toLowerCase().equals("lz4"))
|
||||
{
|
||||
options.put(StartupMessage.COMPRESSION, "lz4");
|
||||
connection.setCompressor(Compressor.LZ4Compressor.instance);
|
||||
}
|
||||
if (next.toLowerCase().equals("throw_on_overload"))
|
||||
{
|
||||
options.put(StartupMessage.THROW_ON_OVERLOAD, "1");
|
||||
connection.setThrowOnOverload(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (checksumType == null)
|
||||
{
|
||||
if (compressor != null)
|
||||
{
|
||||
connection.setTransformer(CompressingTransformer.getTransformer(compressor));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connection.setTransformer(ChecksummingTransformer.getTransformer(checksumType, compressor));
|
||||
}
|
||||
|
||||
return new StartupMessage(options);
|
||||
}
|
||||
else if (msgType.equals("QUERY"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.codahale.metrics.Reservoir;
|
||||
import com.codahale.metrics.Snapshot;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.metrics.DecayingEstimatedHistogramReservoir;
|
||||
import org.apache.cassandra.net.AbstractMessageHandler;
|
||||
import org.apache.cassandra.net.ResourceLimits;
|
||||
|
||||
public class ClientResourceLimits
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClientResourceLimits.class);
|
||||
|
||||
private static final ResourceLimits.Concurrent GLOBAL_LIMIT = new ResourceLimits.Concurrent(getGlobalLimit());
|
||||
private static final AbstractMessageHandler.WaitQueue GLOBAL_QUEUE = AbstractMessageHandler.WaitQueue.global(GLOBAL_LIMIT);
|
||||
private static final ConcurrentMap<InetAddress, Allocator> PER_ENDPOINT_ALLOCATORS = new ConcurrentHashMap<>();
|
||||
|
||||
public static Allocator getAllocatorForEndpoint(InetAddress endpoint)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Allocator result = PER_ENDPOINT_ALLOCATORS.computeIfAbsent(endpoint, Allocator::new);
|
||||
if (result.acquire())
|
||||
return result;
|
||||
|
||||
PER_ENDPOINT_ALLOCATORS.remove(endpoint, result);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getGlobalLimit()
|
||||
{
|
||||
return DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytes();
|
||||
}
|
||||
|
||||
public static void setGlobalLimit(long newLimit)
|
||||
{
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytes(newLimit);
|
||||
long existingLimit = GLOBAL_LIMIT.setLimit(getGlobalLimit());
|
||||
logger.info("Changed native_max_transport_requests_in_bytes from {} to {}", existingLimit, newLimit);
|
||||
}
|
||||
|
||||
public static long getCurrentGlobalUsage()
|
||||
{
|
||||
return GLOBAL_LIMIT.using();
|
||||
}
|
||||
|
||||
public static long getEndpointLimit()
|
||||
{
|
||||
return DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp();
|
||||
}
|
||||
|
||||
public static void setEndpointLimit(long newLimit)
|
||||
{
|
||||
long existingLimit = DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp();
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytesPerIp(newLimit); // ensure new instances get the new limit
|
||||
for (Allocator allocator : PER_ENDPOINT_ALLOCATORS.values())
|
||||
existingLimit = allocator.endpointAndGlobal.endpoint().setLimit(newLimit);
|
||||
logger.info("Changed native_max_transport_requests_in_bytes_per_ip from {} to {}", existingLimit, newLimit);
|
||||
}
|
||||
|
||||
public static Snapshot getCurrentIpUsage()
|
||||
{
|
||||
DecayingEstimatedHistogramReservoir histogram = new DecayingEstimatedHistogramReservoir();
|
||||
for (Allocator allocator : PER_ENDPOINT_ALLOCATORS.values())
|
||||
{
|
||||
histogram.update(allocator.endpointAndGlobal.endpoint().using());
|
||||
}
|
||||
return histogram.getSnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* This will recompute the ip usage histo on each query of the snapshot when requested instead of trying to keep
|
||||
* a histogram up to date with each request
|
||||
*/
|
||||
public static Reservoir ipUsageReservoir()
|
||||
{
|
||||
return new Reservoir()
|
||||
{
|
||||
public int size()
|
||||
{
|
||||
return PER_ENDPOINT_ALLOCATORS.size();
|
||||
}
|
||||
|
||||
public void update(long l)
|
||||
{
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
public Snapshot getSnapshot()
|
||||
{
|
||||
return getCurrentIpUsage();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Used during protocol negotiation in {@link InitialConnectionHandler} and then if protocol V4 or earlier is
|
||||
* selected. V5 connections convert this to a {@link ResourceProvider} and use the global/endpoint
|
||||
* limits and queues directly.
|
||||
*/
|
||||
static class Allocator
|
||||
{
|
||||
private final AtomicInteger refCount = new AtomicInteger(0);
|
||||
private final InetAddress endpoint;
|
||||
|
||||
private final ResourceLimits.EndpointAndGlobal endpointAndGlobal;
|
||||
// This is not used or exposed by instances of this class, but it is used for V5+
|
||||
// client connections so it's initialized here as the same queue must be shared
|
||||
// across all clients from the same remote address.
|
||||
private final AbstractMessageHandler.WaitQueue waitQueue;
|
||||
|
||||
private Allocator(InetAddress endpoint)
|
||||
{
|
||||
this.endpoint = endpoint;
|
||||
ResourceLimits.Concurrent limit = new ResourceLimits.Concurrent(getEndpointLimit());
|
||||
endpointAndGlobal = new ResourceLimits.EndpointAndGlobal(limit, GLOBAL_LIMIT);
|
||||
waitQueue = AbstractMessageHandler.WaitQueue.endpoint(limit);
|
||||
}
|
||||
|
||||
private boolean acquire()
|
||||
{
|
||||
return 0 < refCount.updateAndGet(i -> i < 0 ? i : i + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the reference count, possibly removing the instance from the cache
|
||||
* if this is its final reference
|
||||
*/
|
||||
void release()
|
||||
{
|
||||
if (-1 == refCount.updateAndGet(i -> i == 1 ? -1 : i - 1))
|
||||
PER_ENDPOINT_ALLOCATORS.remove(endpoint, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to allocate a number of permits representing bytes towards the inflight
|
||||
* limits. To succeed, it must be possible to allocate from both the per-endpoint
|
||||
* and global reserves.
|
||||
*
|
||||
* @param amount number permits to allocate
|
||||
* @return outcome SUCCESS if the allocation was successful. In the case of failure,
|
||||
* either INSUFFICIENT_GLOBAL or INSUFFICIENT_ENPOINT to indicate which reserve rejected
|
||||
* the allocation request.
|
||||
*/
|
||||
ResourceLimits.Outcome tryAllocate(long amount)
|
||||
{
|
||||
return endpointAndGlobal.tryAllocate(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force an allocation of a number of permits representing bytes from the inflight
|
||||
* limits. Permits will be acquired from both the per-endpoint and global reserves
|
||||
* which may lead to either or both reserves going over their limits.
|
||||
* @param amount number permits to allocate
|
||||
*/
|
||||
void allocate(long amount)
|
||||
{
|
||||
endpointAndGlobal.allocate(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a number of permits representing bytes back to the both the per-endpoint and
|
||||
* global limits for inflight requests.
|
||||
*
|
||||
* @param amount number of permits to release
|
||||
* @return outcome, ABOVE_LIMIT if either reserve is above its configured limit after
|
||||
* the operation completes or, BELOW_LIMIT if neither is.
|
||||
* rejected the allocation request.
|
||||
*/
|
||||
ResourceLimits.Outcome release(long amount)
|
||||
{
|
||||
return endpointAndGlobal.release(amount);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
long endpointUsing()
|
||||
{
|
||||
return endpointAndGlobal.endpoint().using();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
long globallyUsing()
|
||||
{
|
||||
return endpointAndGlobal.global().using();
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return String.format("InflightEndpointRequestPayload: %d, InflightOverallRequestPayload: %d",
|
||||
endpointAndGlobal.endpoint().using(), endpointAndGlobal.global().using());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in protocol V5 and later by the AbstractMessageHandler/CQLMessageHandler hierarchy.
|
||||
* This hides the allocate/tryAllocate/release methods from EndpointResourceLimits and exposes
|
||||
* the endpoint and global limits, along with their corresponding
|
||||
* {@link org.apache.cassandra.net.AbstractMessageHandler.WaitQueue} directly.
|
||||
* Provided as an interface and single implementation for testing (see CQLConnectionTest)
|
||||
*/
|
||||
interface ResourceProvider
|
||||
{
|
||||
ResourceLimits.Limit globalLimit();
|
||||
AbstractMessageHandler.WaitQueue globalWaitQueue();
|
||||
ResourceLimits.Limit endpointLimit();
|
||||
AbstractMessageHandler.WaitQueue endpointWaitQueue();
|
||||
void release();
|
||||
|
||||
static class Default implements ResourceProvider
|
||||
{
|
||||
private final Allocator limits;
|
||||
|
||||
Default(Allocator limits)
|
||||
{
|
||||
this.limits = limits;
|
||||
}
|
||||
|
||||
public ResourceLimits.Limit globalLimit()
|
||||
{
|
||||
return limits.endpointAndGlobal.global();
|
||||
}
|
||||
|
||||
public AbstractMessageHandler.WaitQueue globalWaitQueue()
|
||||
{
|
||||
return GLOBAL_QUEUE;
|
||||
}
|
||||
|
||||
public ResourceLimits.Limit endpointLimit()
|
||||
{
|
||||
return limits.endpointAndGlobal.endpoint();
|
||||
}
|
||||
|
||||
public AbstractMessageHandler.WaitQueue endpointWaitQueue()
|
||||
{
|
||||
return limits.waitQueue;
|
||||
}
|
||||
|
||||
public void release()
|
||||
{
|
||||
limits.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.xerial.snappy.Snappy;
|
||||
import org.xerial.snappy.SnappyError;
|
||||
|
||||
import net.jpountz.lz4.LZ4Factory;
|
||||
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
|
||||
public interface Compressor
|
||||
{
|
||||
public Envelope compress(Envelope uncompressed) throws IOException;
|
||||
public Envelope decompress(Envelope compressed) throws IOException;
|
||||
|
||||
/*
|
||||
* TODO: We can probably do more efficient, like by avoiding copy.
|
||||
* Also, we don't reuse ICompressor because the API doesn't expose enough.
|
||||
*/
|
||||
public static class SnappyCompressor implements Compressor
|
||||
{
|
||||
public static final SnappyCompressor instance;
|
||||
static
|
||||
{
|
||||
SnappyCompressor i;
|
||||
try
|
||||
{
|
||||
i = new SnappyCompressor();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
JVMStabilityInspector.inspectThrowable(e);
|
||||
i = null;
|
||||
}
|
||||
catch (NoClassDefFoundError | SnappyError | UnsatisfiedLinkError e)
|
||||
{
|
||||
i = null;
|
||||
}
|
||||
instance = i;
|
||||
}
|
||||
|
||||
private SnappyCompressor()
|
||||
{
|
||||
// this would throw java.lang.NoClassDefFoundError if Snappy class
|
||||
// wasn't found at runtime which should be processed by the calling method
|
||||
Snappy.getNativeLibraryVersion();
|
||||
}
|
||||
|
||||
public Envelope compress(Envelope uncompressed) throws IOException
|
||||
{
|
||||
byte[] input = CBUtil.readRawBytes(uncompressed.body);
|
||||
ByteBuf output = CBUtil.allocator.heapBuffer(Snappy.maxCompressedLength(input.length));
|
||||
|
||||
try
|
||||
{
|
||||
int written = Snappy.compress(input, 0, input.length, output.array(), output.arrayOffset());
|
||||
output.writerIndex(written);
|
||||
}
|
||||
catch (final Throwable e)
|
||||
{
|
||||
output.release();
|
||||
throw e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
uncompressed.release();
|
||||
}
|
||||
|
||||
return uncompressed.with(output);
|
||||
}
|
||||
|
||||
public Envelope decompress(Envelope compressed) throws IOException
|
||||
{
|
||||
byte[] input = CBUtil.readRawBytes(compressed.body);
|
||||
|
||||
if (!Snappy.isValidCompressedBuffer(input, 0, input.length))
|
||||
throw new ProtocolException("Provided frame does not appear to be Snappy compressed");
|
||||
|
||||
ByteBuf output = CBUtil.allocator.heapBuffer(Snappy.uncompressedLength(input));
|
||||
|
||||
try
|
||||
{
|
||||
int size = Snappy.uncompress(input, 0, input.length, output.array(), output.arrayOffset());
|
||||
output.writerIndex(size);
|
||||
}
|
||||
catch (final Throwable e)
|
||||
{
|
||||
output.release();
|
||||
throw e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
compressed.release();
|
||||
}
|
||||
|
||||
return compressed.with(output);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is very close to the ICompressor implementation, and in particular
|
||||
* it also layout the uncompressed size at the beginning of the message to
|
||||
* make uncompression faster, but contrarly to the ICompressor, that length
|
||||
* is written in big-endian. The native protocol is entirely big-endian, so
|
||||
* it feels like putting little-endian here would be a annoying trap for
|
||||
* client writer.
|
||||
*/
|
||||
public static class LZ4Compressor implements Compressor
|
||||
{
|
||||
public static final LZ4Compressor instance = new LZ4Compressor();
|
||||
|
||||
private static final int INTEGER_BYTES = 4;
|
||||
private final net.jpountz.lz4.LZ4Compressor compressor;
|
||||
private final net.jpountz.lz4.LZ4Decompressor decompressor;
|
||||
|
||||
private LZ4Compressor()
|
||||
{
|
||||
final LZ4Factory lz4Factory = LZ4Factory.fastestInstance();
|
||||
compressor = lz4Factory.fastCompressor();
|
||||
decompressor = lz4Factory.decompressor();
|
||||
}
|
||||
|
||||
public Envelope compress(Envelope uncompressed)
|
||||
{
|
||||
byte[] input = CBUtil.readRawBytes(uncompressed.body);
|
||||
|
||||
int maxCompressedLength = compressor.maxCompressedLength(input.length);
|
||||
ByteBuf outputBuf = CBUtil.allocator.heapBuffer(INTEGER_BYTES + maxCompressedLength);
|
||||
|
||||
byte[] output = outputBuf.array();
|
||||
int outputOffset = outputBuf.arrayOffset();
|
||||
|
||||
output[outputOffset ] = (byte) (input.length >>> 24);
|
||||
output[outputOffset + 1] = (byte) (input.length >>> 16);
|
||||
output[outputOffset + 2] = (byte) (input.length >>> 8);
|
||||
output[outputOffset + 3] = (byte) (input.length);
|
||||
|
||||
try
|
||||
{
|
||||
int written = compressor.compress(input, 0, input.length, output, outputOffset + INTEGER_BYTES, maxCompressedLength);
|
||||
outputBuf.writerIndex(INTEGER_BYTES + written);
|
||||
|
||||
return uncompressed.with(outputBuf);
|
||||
}
|
||||
catch (final Throwable e)
|
||||
{
|
||||
outputBuf.release();
|
||||
throw e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
uncompressed.release();
|
||||
}
|
||||
}
|
||||
|
||||
public Envelope decompress(Envelope compressed) throws IOException
|
||||
{
|
||||
byte[] input = CBUtil.readRawBytes(compressed.body);
|
||||
|
||||
int uncompressedLength = ((input[0] & 0xFF) << 24)
|
||||
| ((input[1] & 0xFF) << 16)
|
||||
| ((input[2] & 0xFF) << 8)
|
||||
| ((input[3] & 0xFF));
|
||||
|
||||
ByteBuf output = CBUtil.allocator.heapBuffer(uncompressedLength);
|
||||
|
||||
try
|
||||
{
|
||||
int read = decompressor.decompress(input, INTEGER_BYTES, output.array(), output.arrayOffset(), uncompressedLength);
|
||||
if (read != input.length - INTEGER_BYTES)
|
||||
throw new IOException("Compressed lengths mismatch");
|
||||
|
||||
output.writerIndex(uncompressedLength);
|
||||
|
||||
return compressed.with(output);
|
||||
}
|
||||
catch (final Throwable e)
|
||||
{
|
||||
output.release();
|
||||
throw e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
//release the old message
|
||||
compressed.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ package org.apache.cassandra.transport;
|
|||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.apache.cassandra.transport.frame.FrameBodyTransformer;
|
||||
|
||||
public class Connection
|
||||
{
|
||||
|
|
@ -29,8 +28,8 @@ public class Connection
|
|||
private final ProtocolVersion version;
|
||||
private final Tracker tracker;
|
||||
|
||||
private volatile FrameBodyTransformer transformer;
|
||||
private boolean throwOnOverload;
|
||||
private volatile Compressor preV5MessageCompressor;
|
||||
|
||||
public Connection(Channel channel, ProtocolVersion version, Tracker tracker)
|
||||
{
|
||||
|
|
@ -41,14 +40,14 @@ public class Connection
|
|||
tracker.addConnection(channel, this);
|
||||
}
|
||||
|
||||
public void setTransformer(FrameBodyTransformer transformer)
|
||||
public void setCompressor(Compressor compressor)
|
||||
{
|
||||
this.transformer = transformer;
|
||||
this.preV5MessageCompressor = compressor;
|
||||
}
|
||||
|
||||
public FrameBodyTransformer getTransformer()
|
||||
public Compressor getCompressor()
|
||||
{
|
||||
return transformer;
|
||||
return preV5MessageCompressor;
|
||||
}
|
||||
|
||||
public void setThrowOnOverload(boolean throwOnOverload)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.EventLoop;
|
||||
import org.apache.cassandra.concurrent.LocalAwareExecutorService;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.service.ClientWarn;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.Flusher.FlushItem;
|
||||
import org.apache.cassandra.transport.messages.ErrorMessage;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
|
||||
import static org.apache.cassandra.concurrent.SharedExecutorPool.SHARED;
|
||||
|
||||
public class Dispatcher
|
||||
{
|
||||
private static final LocalAwareExecutorService requestExecutor = SHARED.newExecutor(DatabaseDescriptor.getNativeTransportMaxThreads(),
|
||||
DatabaseDescriptor::setNativeTransportMaxThreads,
|
||||
"transport",
|
||||
"Native-Transport-Requests");
|
||||
|
||||
private static final ConcurrentMap<EventLoop, Flusher> flusherLookup = new ConcurrentHashMap<>();
|
||||
private final boolean useLegacyFlusher;
|
||||
|
||||
/**
|
||||
* Takes a Channel, Request and the Response produced by processRequest and outputs a FlushItem
|
||||
* appropriate for the pipeline, which is specific to the protocol version. V5 and above will
|
||||
* produce FlushItem.Framed instances whereas earlier versions require FlushItem.Unframed.
|
||||
* The instances of these FlushItem subclasses are specialized to release resources in the
|
||||
* right way for the specific pipeline that produced them.
|
||||
*/
|
||||
// TODO parameterize with FlushItem subclass
|
||||
interface FlushItemConverter
|
||||
{
|
||||
FlushItem<?> toFlushItem(Channel channel, Message.Request request, Message.Response response);
|
||||
}
|
||||
|
||||
public Dispatcher(boolean useLegacyFlusher)
|
||||
{
|
||||
this.useLegacyFlusher = useLegacyFlusher;
|
||||
}
|
||||
|
||||
public void dispatch(Channel channel, Message.Request request, FlushItemConverter forFlusher) {
|
||||
requestExecutor.submit(() -> processRequest(channel, request, forFlusher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this method may be executed on the netty event loop, during initial protocol negotiation
|
||||
*/
|
||||
static Message.Response processRequest(ServerConnection connection, Message.Request request)
|
||||
{
|
||||
long queryStartNanoTime = System.nanoTime();
|
||||
if (connection.getVersion().isGreaterOrEqualTo(ProtocolVersion.V4))
|
||||
ClientWarn.instance.captureWarnings();
|
||||
|
||||
QueryState qstate = connection.validateNewMessage(request.type, connection.getVersion());
|
||||
|
||||
Message.logger.trace("Received: {}, v={}", request, connection.getVersion());
|
||||
connection.requests.inc();
|
||||
Message.Response response = request.execute(qstate, queryStartNanoTime);
|
||||
response.setStreamId(request.getStreamId());
|
||||
response.setWarnings(ClientWarn.instance.getWarnings());
|
||||
response.attach(connection);
|
||||
connection.applyStateTransition(request.type, response.type);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this method is not expected to execute on the netty event loop.
|
||||
*/
|
||||
void processRequest(Channel channel, Message.Request request, FlushItemConverter forFlusher)
|
||||
{
|
||||
final Message.Response response;
|
||||
final ServerConnection connection;
|
||||
FlushItem<?> toFlush;
|
||||
try
|
||||
{
|
||||
assert request.connection() instanceof ServerConnection;
|
||||
connection = (ServerConnection) request.connection();
|
||||
response = processRequest(connection, request);
|
||||
toFlush = forFlusher.toFlushItem(channel, request, response);
|
||||
Message.logger.trace("Responding: {}, v={}", response, connection.getVersion());
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
JVMStabilityInspector.inspectThrowable(t);
|
||||
ExceptionHandlers.UnexpectedChannelExceptionHandler handler = new ExceptionHandlers.UnexpectedChannelExceptionHandler(channel, true);
|
||||
ErrorMessage error = ErrorMessage.fromException(t, handler);
|
||||
error.setStreamId(request.getStreamId());
|
||||
toFlush = forFlusher.toFlushItem(channel, request, error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ClientWarn.instance.resetWarnings();
|
||||
}
|
||||
flush(toFlush);
|
||||
}
|
||||
|
||||
private void flush(FlushItem<?> item)
|
||||
{
|
||||
EventLoop loop = item.channel.eventLoop();
|
||||
Flusher flusher = flusherLookup.get(loop);
|
||||
if (flusher == null)
|
||||
{
|
||||
Flusher created = useLegacyFlusher ? Flusher.legacy(loop) : Flusher.immediate(loop);
|
||||
Flusher alt = flusherLookup.putIfAbsent(loop, flusher = created);
|
||||
if (alt != null)
|
||||
flusher = alt;
|
||||
}
|
||||
|
||||
flusher.enqueue(item);
|
||||
flusher.start();
|
||||
}
|
||||
|
||||
public static void shutdown()
|
||||
{
|
||||
if (requestExecutor != null)
|
||||
{
|
||||
requestExecutor.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,24 +19,26 @@
|
|||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
import io.netty.util.Attribute;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.metrics.ClientRequestSizeMetrics;
|
||||
import org.apache.cassandra.transport.frame.FrameBodyTransformer;
|
||||
import org.apache.cassandra.metrics.ClientMessageSizeMetrics;
|
||||
import org.apache.cassandra.transport.messages.ErrorMessage;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
|
||||
public class Frame
|
||||
public class Envelope
|
||||
{
|
||||
public static final byte PROTOCOL_VERSION_MASK = 0x7f;
|
||||
|
||||
|
|
@ -44,7 +46,7 @@ public class Frame
|
|||
public final ByteBuf body;
|
||||
|
||||
/**
|
||||
* An on-wire frame consists of a header and a body.
|
||||
* An on-wire message envelope consists of a header and a body.
|
||||
*
|
||||
* The header is defined the following way in native protocol version 3 and later:
|
||||
*
|
||||
|
|
@ -55,7 +57,7 @@ public class Frame
|
|||
* | length |
|
||||
* +---------+---------+---------+---------+
|
||||
*/
|
||||
private Frame(Header header, ByteBuf body)
|
||||
public Envelope(Header header, ByteBuf body)
|
||||
{
|
||||
this.header = header;
|
||||
this.body = body;
|
||||
|
|
@ -71,10 +73,36 @@ public class Frame
|
|||
return body.release();
|
||||
}
|
||||
|
||||
public static Frame create(Message.Type type, int streamId, ProtocolVersion version, EnumSet<Header.Flag> flags, ByteBuf body)
|
||||
@VisibleForTesting
|
||||
public Envelope clone()
|
||||
{
|
||||
return new Envelope(header, Unpooled.wrappedBuffer(ByteBufferUtil.clone(body.nioBuffer())));
|
||||
}
|
||||
|
||||
public static Envelope create(Message.Type type, int streamId, ProtocolVersion version, EnumSet<Header.Flag> flags, ByteBuf body)
|
||||
{
|
||||
Header header = new Header(version, flags, streamId, type, body.readableBytes());
|
||||
return new Frame(header, body);
|
||||
return new Envelope(header, body);
|
||||
}
|
||||
|
||||
public void encodeHeaderInto(ByteBuffer buf)
|
||||
{
|
||||
buf.put((byte) header.type.direction.addToVersion(header.version.asInt()));
|
||||
buf.put((byte) Envelope.Header.Flag.serialize(header.flags));
|
||||
|
||||
if (header.version.isGreaterOrEqualTo(ProtocolVersion.V3))
|
||||
buf.putShort((short) header.streamId);
|
||||
else
|
||||
buf.put((byte) header.streamId);
|
||||
|
||||
buf.put((byte) header.type.opcode);
|
||||
buf.putInt(body.readableBytes());
|
||||
}
|
||||
|
||||
public void encodeInto(ByteBuffer buf)
|
||||
{
|
||||
encodeHeaderInto(buf);
|
||||
buf.put(body.nioBuffer());
|
||||
}
|
||||
|
||||
public static class Header
|
||||
|
|
@ -106,8 +134,7 @@ public class Frame
|
|||
TRACING,
|
||||
CUSTOM_PAYLOAD,
|
||||
WARNING,
|
||||
USE_BETA,
|
||||
CHECKSUMMED;
|
||||
USE_BETA;
|
||||
|
||||
private static final Flag[] ALL_VALUES = values();
|
||||
|
||||
|
|
@ -132,32 +159,70 @@ public class Frame
|
|||
}
|
||||
}
|
||||
|
||||
public Frame with(ByteBuf newBody)
|
||||
public Envelope with(ByteBuf newBody)
|
||||
{
|
||||
return new Frame(header, newBody);
|
||||
return new Envelope(header, newBody);
|
||||
}
|
||||
|
||||
public static class Decoder extends ByteToMessageDecoder
|
||||
{
|
||||
private static final int MAX_FRAME_LENGTH = DatabaseDescriptor.getNativeTransportMaxFrameSize();
|
||||
private static final int MAX_TOTAL_LENGTH = DatabaseDescriptor.getNativeTransportMaxFrameSize();
|
||||
|
||||
private boolean discardingTooLongFrame;
|
||||
private long tooLongFrameLength;
|
||||
private boolean discardingTooLongMessage;
|
||||
private long tooLongTotalLength;
|
||||
private long bytesToDiscard;
|
||||
private int tooLongStreamId;
|
||||
|
||||
private final Connection.Factory factory;
|
||||
|
||||
public Decoder(Connection.Factory factory)
|
||||
/**
|
||||
* Used by protocol V5 and later to extract a CQL message header from the buffer containing
|
||||
* it, without modifying the position of the underlying buffer. This essentially mirrors the
|
||||
* pre-V5 code in {@link org.apache.cassandra.transport.Envelope.Decoder#decode(ByteBuf)},
|
||||
* with two differences:
|
||||
* The input is a ByteBuffer rather than a ByteBuf
|
||||
* This cannot return null, as V5 always deals with entire CQL messages. Coalescing of bytes
|
||||
* off the wire happens at the layer below, in {@link org.apache.cassandra.net.FrameDecoder}
|
||||
*
|
||||
* @param buffer ByteBuffer containing the message envelope
|
||||
* @return CQL Message header
|
||||
*/
|
||||
Envelope.Header extractHeader(ByteBuffer buffer)
|
||||
{
|
||||
this.factory = factory;
|
||||
Preconditions.checkArgument(buffer.remaining() >= Header.LENGTH,
|
||||
"Undersized buffer supplied. Expected %s, actual %s",
|
||||
Header.LENGTH,
|
||||
buffer.remaining());
|
||||
|
||||
int idx = buffer.position();
|
||||
int firstByte = buffer.get(idx++);
|
||||
Message.Direction direction = Message.Direction.extractFromVersion(firstByte);
|
||||
int versionNum = firstByte & PROTOCOL_VERSION_MASK;
|
||||
ProtocolVersion version = ProtocolVersion.decode(versionNum, DatabaseDescriptor.getNativeTransportAllowOlderProtocols());
|
||||
|
||||
int flags = buffer.get(idx++);
|
||||
EnumSet<Header.Flag> decodedFlags = decodeFlags(version, flags);
|
||||
|
||||
int streamId = buffer.getShort(idx);
|
||||
idx += 2;
|
||||
|
||||
// This throws a protocol exceptions if the opcode is unknown
|
||||
Message.Type type;
|
||||
try
|
||||
{
|
||||
type = Message.Type.fromOpcode(buffer.get(idx++), direction);
|
||||
}
|
||||
catch (ProtocolException e)
|
||||
{
|
||||
throw ErrorMessage.wrap(e, streamId);
|
||||
}
|
||||
|
||||
long bodyLength = buffer.getInt(idx);
|
||||
return new Header(version, decodedFlags, streamId, type, bodyLength);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Frame decodeFrame(ByteBuf buffer)
|
||||
throws Exception
|
||||
Envelope decode(ByteBuf buffer)
|
||||
{
|
||||
if (discardingTooLongFrame)
|
||||
if (discardingTooLongMessage)
|
||||
{
|
||||
bytesToDiscard = discard(buffer, bytesToDiscard);
|
||||
// If we have discarded everything, throw the exception
|
||||
|
|
@ -184,11 +249,7 @@ public class Frame
|
|||
return null;
|
||||
|
||||
int flags = buffer.getByte(idx++);
|
||||
EnumSet<Header.Flag> decodedFlags = Header.Flag.deserialize(flags);
|
||||
|
||||
if (version.isBeta() && !decodedFlags.contains(Header.Flag.USE_BETA))
|
||||
throw new ProtocolException(String.format("Beta version of the protocol used (%s), but USE_BETA flag is unset", version),
|
||||
version);
|
||||
EnumSet<Header.Flag> decodedFlags = decodeFlags(version, flags);
|
||||
|
||||
int streamId = buffer.getShort(idx);
|
||||
idx += 2;
|
||||
|
|
@ -207,24 +268,24 @@ public class Frame
|
|||
long bodyLength = buffer.getUnsignedInt(idx);
|
||||
idx += Header.BODY_LENGTH_SIZE;
|
||||
|
||||
long frameLength = bodyLength + Header.LENGTH;
|
||||
if (frameLength > MAX_FRAME_LENGTH)
|
||||
long totalLength = bodyLength + Header.LENGTH;
|
||||
if (totalLength > MAX_TOTAL_LENGTH)
|
||||
{
|
||||
// Enter the discard mode and discard everything received so far.
|
||||
discardingTooLongFrame = true;
|
||||
discardingTooLongMessage = true;
|
||||
tooLongStreamId = streamId;
|
||||
tooLongFrameLength = frameLength;
|
||||
bytesToDiscard = discard(buffer, frameLength);
|
||||
tooLongTotalLength = totalLength;
|
||||
bytesToDiscard = discard(buffer, totalLength);
|
||||
if (bytesToDiscard <= 0)
|
||||
fail();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (buffer.readableBytes() < frameLength)
|
||||
if (buffer.readableBytes() < totalLength)
|
||||
return null;
|
||||
|
||||
ClientRequestSizeMetrics.totalBytesRead.inc(frameLength);
|
||||
ClientRequestSizeMetrics.bytesReceivedPerFrame.update(frameLength);
|
||||
ClientMessageSizeMetrics.bytesReceived.inc(totalLength);
|
||||
ClientMessageSizeMetrics.bytesReceivedPerRequest.update(totalLength);
|
||||
|
||||
// extract body
|
||||
ByteBuf body = buffer.slice(idx, (int) bodyLength);
|
||||
|
|
@ -233,42 +294,36 @@ public class Frame
|
|||
idx += bodyLength;
|
||||
buffer.readerIndex(idx);
|
||||
|
||||
return new Frame(new Header(version, decodedFlags, streamId, type, bodyLength), body);
|
||||
return new Envelope(new Header(version, decodedFlags, streamId, type, bodyLength), body);
|
||||
}
|
||||
|
||||
private EnumSet<Header.Flag> decodeFlags(ProtocolVersion version, int flags)
|
||||
{
|
||||
EnumSet<Header.Flag> decodedFlags = Header.Flag.deserialize(flags);
|
||||
|
||||
if (version.isBeta() && !decodedFlags.contains(Header.Flag.USE_BETA))
|
||||
throw new ProtocolException(String.format("Beta version of the protocol used (%s), but USE_BETA flag is unset", version),
|
||||
version);
|
||||
return decodedFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> results)
|
||||
throws Exception
|
||||
{
|
||||
Frame frame = decodeFrame(buffer);
|
||||
if (frame == null) return;
|
||||
Envelope envelope = decode(buffer);
|
||||
if (envelope == null)
|
||||
return;
|
||||
|
||||
Attribute<Connection> attrConn = ctx.channel().attr(Connection.attributeKey);
|
||||
Connection connection = attrConn.get();
|
||||
if (connection == null)
|
||||
{
|
||||
// First message seen on this channel, attach the connection object
|
||||
connection = factory.newConnection(ctx.channel(), frame.header.version);
|
||||
attrConn.set(connection);
|
||||
}
|
||||
else if (connection.getVersion() != frame.header.version)
|
||||
{
|
||||
throw ErrorMessage.wrap(
|
||||
new ProtocolException(String.format(
|
||||
"Invalid message version. Got %s but previous messages on this connection had version %s",
|
||||
frame.header.version, connection.getVersion())),
|
||||
frame.header.streamId);
|
||||
}
|
||||
results.add(frame);
|
||||
results.add(envelope);
|
||||
}
|
||||
|
||||
private void fail()
|
||||
{
|
||||
// Reset to the initial state and throw the exception
|
||||
long tooLongFrameLength = this.tooLongFrameLength;
|
||||
this.tooLongFrameLength = 0;
|
||||
discardingTooLongFrame = false;
|
||||
String msg = String.format("Request is too big: length %d exceeds maximum allowed length %d.", tooLongFrameLength, MAX_FRAME_LENGTH);
|
||||
long tooLongTotalLength = this.tooLongTotalLength;
|
||||
this.tooLongTotalLength = 0;
|
||||
discardingTooLongMessage = false;
|
||||
String msg = String.format("Request is too big: length %d exceeds maximum allowed length %d.", tooLongTotalLength, MAX_TOTAL_LENGTH);
|
||||
throw ErrorMessage.wrap(new InvalidRequestException(msg), tooLongStreamId);
|
||||
}
|
||||
}
|
||||
|
|
@ -282,101 +337,97 @@ public class Frame
|
|||
}
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public static class Encoder extends MessageToMessageEncoder<Frame>
|
||||
public static class Encoder extends MessageToMessageEncoder<Envelope>
|
||||
{
|
||||
public void encode(ChannelHandlerContext ctx, Frame frame, List<Object> results)
|
||||
throws IOException
|
||||
{
|
||||
ByteBuf header = CBUtil.allocator.buffer(Header.LENGTH);
|
||||
public static final Encoder instance = new Envelope.Encoder();
|
||||
private Encoder(){}
|
||||
|
||||
Message.Type type = frame.header.type;
|
||||
header.writeByte(type.direction.addToVersion(frame.header.version.asInt()));
|
||||
header.writeByte(Header.Flag.serialize(frame.header.flags));
|
||||
public void encode(ChannelHandlerContext ctx, Envelope source, List<Object> results)
|
||||
{
|
||||
ByteBuf serializedHeader = encodeHeader(source);
|
||||
int messageSize = serializedHeader.readableBytes() + source.body.readableBytes();
|
||||
ClientMessageSizeMetrics.bytesSent.inc(messageSize);
|
||||
ClientMessageSizeMetrics.bytesSentPerResponse.update(messageSize);
|
||||
|
||||
results.add(serializedHeader);
|
||||
results.add(source.body);
|
||||
}
|
||||
|
||||
public ByteBuf encodeHeader(Envelope source)
|
||||
{
|
||||
ByteBuf buf = CBUtil.allocator.buffer(Header.LENGTH);
|
||||
|
||||
Message.Type type = source.header.type;
|
||||
buf.writeByte(type.direction.addToVersion(source.header.version.asInt()));
|
||||
buf.writeByte(Header.Flag.serialize(source.header.flags));
|
||||
|
||||
// Continue to support writing pre-v3 headers so that we can give proper error messages to drivers that
|
||||
// connect with the v1/v2 protocol. See CASSANDRA-11464.
|
||||
if (frame.header.version.isGreaterOrEqualTo(ProtocolVersion.V3))
|
||||
header.writeShort(frame.header.streamId);
|
||||
if (source.header.version.isGreaterOrEqualTo(ProtocolVersion.V3))
|
||||
buf.writeShort(source.header.streamId);
|
||||
else
|
||||
header.writeByte(frame.header.streamId);
|
||||
buf.writeByte(source.header.streamId);
|
||||
|
||||
header.writeByte(type.opcode);
|
||||
header.writeInt(frame.body.readableBytes());
|
||||
|
||||
int messageSize = header.readableBytes() + frame.body.readableBytes();
|
||||
ClientRequestSizeMetrics.totalBytesWritten.inc(messageSize);
|
||||
ClientRequestSizeMetrics.bytesTransmittedPerFrame.update(messageSize);
|
||||
|
||||
results.add(header);
|
||||
results.add(frame.body);
|
||||
buf.writeByte(type.opcode);
|
||||
buf.writeInt(source.body.readableBytes());
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public static class InboundBodyTransformer extends MessageToMessageDecoder<Frame>
|
||||
public static class Decompressor extends MessageToMessageDecoder<Envelope>
|
||||
{
|
||||
public void decode(ChannelHandlerContext ctx, Frame frame, List<Object> results)
|
||||
public static Decompressor instance = new Envelope.Decompressor();
|
||||
private Decompressor(){}
|
||||
|
||||
public void decode(ChannelHandlerContext ctx, Envelope source, List<Object> results)
|
||||
throws IOException
|
||||
{
|
||||
Connection connection = ctx.channel().attr(Connection.attributeKey).get();
|
||||
|
||||
if ((!frame.header.flags.contains(Header.Flag.COMPRESSED) && !frame.header.flags.contains(Header.Flag.CHECKSUMMED)) || connection == null)
|
||||
if (!source.header.flags.contains(Header.Flag.COMPRESSED) || connection == null)
|
||||
{
|
||||
results.add(frame);
|
||||
results.add(source);
|
||||
return;
|
||||
}
|
||||
|
||||
FrameBodyTransformer transformer = connection.getTransformer();
|
||||
if (transformer == null)
|
||||
org.apache.cassandra.transport.Compressor compressor = connection.getCompressor();
|
||||
if (compressor == null)
|
||||
{
|
||||
results.add(frame);
|
||||
results.add(source);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
results.add(frame.with(transformer.transformInbound(frame.body, frame.header.flags)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// release the old frame
|
||||
frame.release();
|
||||
}
|
||||
results.add(compressor.decompress(source));
|
||||
}
|
||||
}
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public static class OutboundBodyTransformer extends MessageToMessageEncoder<Frame>
|
||||
public static class Compressor extends MessageToMessageEncoder<Envelope>
|
||||
{
|
||||
public void encode(ChannelHandlerContext ctx, Frame frame, List<Object> results)
|
||||
public static Compressor instance = new Compressor();
|
||||
private Compressor(){}
|
||||
|
||||
public void encode(ChannelHandlerContext ctx, Envelope source, List<Object> results)
|
||||
throws IOException
|
||||
{
|
||||
Connection connection = ctx.channel().attr(Connection.attributeKey).get();
|
||||
|
||||
// Never transform STARTUP messages
|
||||
if (frame.header.type == Message.Type.STARTUP || connection == null)
|
||||
// Never compress STARTUP messages
|
||||
if (source.header.type == Message.Type.STARTUP || connection == null)
|
||||
{
|
||||
results.add(frame);
|
||||
results.add(source);
|
||||
return;
|
||||
}
|
||||
|
||||
FrameBodyTransformer transformer = connection.getTransformer();
|
||||
if (transformer == null)
|
||||
org.apache.cassandra.transport.Compressor compressor = connection.getCompressor();
|
||||
if (compressor == null)
|
||||
{
|
||||
results.add(frame);
|
||||
results.add(source);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
results.add(frame.with(transformer.transformOutbound(frame.body)));
|
||||
frame.header.flags.addAll(transformer.getOutboundHeaderFlags());
|
||||
}
|
||||
finally
|
||||
{
|
||||
// release the old frame
|
||||
frame.release();
|
||||
}
|
||||
source.header.flags.add(Header.Flag.COMPRESSED);
|
||||
results.add(compressor.compress(source));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import org.apache.cassandra.net.FrameEncoder;
|
||||
import org.apache.cassandra.transport.messages.ErrorMessage;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
|
||||
public class ExceptionHandlers
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExceptionHandlers.class);
|
||||
|
||||
public static ChannelInboundHandlerAdapter postV5Handler(FrameEncoder.PayloadAllocator allocator,
|
||||
ProtocolVersion version)
|
||||
{
|
||||
return new PostV5ExceptionHandler(allocator, version);
|
||||
}
|
||||
|
||||
private static final class PostV5ExceptionHandler extends ChannelInboundHandlerAdapter
|
||||
{
|
||||
private final FrameEncoder.PayloadAllocator allocator;
|
||||
private final ProtocolVersion version;
|
||||
|
||||
public PostV5ExceptionHandler(FrameEncoder.PayloadAllocator allocator, ProtocolVersion version)
|
||||
{
|
||||
this.allocator = allocator;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final ChannelHandlerContext ctx, Throwable cause)
|
||||
{
|
||||
// Provide error message to client in case channel is still open
|
||||
UnexpectedChannelExceptionHandler handler = new UnexpectedChannelExceptionHandler(ctx.channel(), false);
|
||||
if (ctx.channel().isOpen())
|
||||
{
|
||||
ErrorMessage errorMessage = ErrorMessage.fromException(cause, handler);
|
||||
Envelope response = errorMessage.encode(version);
|
||||
FrameEncoder.Payload payload = allocator.allocate(true, CQLMessageHandler.envelopeSize(response.header));
|
||||
try
|
||||
{
|
||||
response.encodeInto(payload.buffer);
|
||||
response.release();
|
||||
payload.finish();
|
||||
ChannelPromise promise = ctx.newPromise();
|
||||
// On protocol exception, close the channel as soon as the message has been sent
|
||||
if (cause instanceof ProtocolException)
|
||||
promise.addListener(future -> ctx.close());
|
||||
ctx.writeAndFlush(payload, promise);
|
||||
}
|
||||
finally
|
||||
{
|
||||
payload.release();
|
||||
JVMStabilityInspector.inspectThrowable(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the channel info in the logged information for unexpected errors, and (if {@link #alwaysLogAtError} is
|
||||
* false then choose the log level based on the type of exception (some are clearly client issues and shouldn't be
|
||||
* logged at server ERROR level)
|
||||
*/
|
||||
static final class UnexpectedChannelExceptionHandler implements Predicate<Throwable>
|
||||
{
|
||||
|
||||
/**
|
||||
* When we encounter an unexpected IOException we look for these {@link Throwable#getMessage() messages}
|
||||
* (because we have no better way to distinguish) and log them at DEBUG rather than INFO, since they
|
||||
* are generally caused by unclean client disconnects rather than an actual problem.
|
||||
*/
|
||||
private static final Set<String> ioExceptionsAtDebugLevel = ImmutableSet.<String>builder().
|
||||
add("Connection reset by peer").
|
||||
add("Broken pipe").
|
||||
add("Connection timed out").
|
||||
build();
|
||||
|
||||
private final Channel channel;
|
||||
private final boolean alwaysLogAtError;
|
||||
|
||||
UnexpectedChannelExceptionHandler(Channel channel, boolean alwaysLogAtError)
|
||||
{
|
||||
this.channel = channel;
|
||||
this.alwaysLogAtError = alwaysLogAtError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Throwable exception)
|
||||
{
|
||||
String message;
|
||||
try
|
||||
{
|
||||
message = "Unexpected exception during request; channel = " + channel;
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
// We don't want to make things worse if String.valueOf() throws an exception
|
||||
message = "Unexpected exception during request; channel = <unprintable>";
|
||||
}
|
||||
|
||||
// netty wraps SSL errors in a CodecExcpetion
|
||||
if (!alwaysLogAtError && (exception instanceof IOException || (exception.getCause() instanceof IOException)))
|
||||
{
|
||||
String errorMessage = exception.getMessage();
|
||||
boolean logAtTrace = false;
|
||||
|
||||
for (String ioException : ioExceptionsAtDebugLevel)
|
||||
{
|
||||
// exceptions thrown from the netty epoll transport add the name of the function that failed
|
||||
// to the exception string (which is simply wrapping a JDK exception), so we can't do a simple/naive comparison
|
||||
if (errorMessage.contains(ioException))
|
||||
{
|
||||
logAtTrace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (logAtTrace)
|
||||
{
|
||||
// Likely unclean client disconnects
|
||||
logger.trace(message, exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generally unhandled IO exceptions are network issues, not actual ERRORS
|
||||
logger.info(message, exception);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Anything else is probably a bug in server of client binary protocol handling
|
||||
logger.error(message, exception);
|
||||
}
|
||||
|
||||
// We handled the exception.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.EventLoop;
|
||||
import org.apache.cassandra.net.FrameEncoder;
|
||||
import org.apache.cassandra.net.FrameEncoderCrc;
|
||||
import org.apache.cassandra.net.FrameEncoderLZ4;
|
||||
import org.apache.cassandra.transport.Message.Response;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.memory.BufferPool;
|
||||
|
||||
import static org.apache.cassandra.transport.CQLMessageHandler.envelopeSize;
|
||||
|
||||
abstract class Flusher implements Runnable
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(Flusher.class);
|
||||
@VisibleForTesting
|
||||
public static final int MAX_FRAMED_PAYLOAD_SIZE =
|
||||
Math.min(BufferPool.NORMAL_CHUNK_SIZE,
|
||||
FrameEncoder.Payload.MAX_SIZE - Math.max(FrameEncoderCrc.HEADER_AND_TRAILER_LENGTH, FrameEncoderLZ4.HEADER_AND_TRAILER_LENGTH));
|
||||
|
||||
static class FlushItem<T>
|
||||
{
|
||||
enum Kind {FRAMED, UNFRAMED}
|
||||
|
||||
final Kind kind;
|
||||
final Channel channel;
|
||||
final T response;
|
||||
final Envelope request;
|
||||
final Consumer<FlushItem<T>> tidy;
|
||||
|
||||
FlushItem(Kind kind, Channel channel, T response, Envelope request, Consumer<FlushItem<T>> tidy)
|
||||
{
|
||||
this.kind = kind;
|
||||
this.channel = channel;
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.tidy = tidy;
|
||||
}
|
||||
|
||||
void release()
|
||||
{
|
||||
tidy.accept(this);
|
||||
}
|
||||
|
||||
static class Framed extends FlushItem<Envelope>
|
||||
{
|
||||
final FrameEncoder.PayloadAllocator allocator;
|
||||
Framed(Channel channel,
|
||||
Envelope response,
|
||||
Envelope request,
|
||||
FrameEncoder.PayloadAllocator allocator,
|
||||
Consumer<FlushItem<Envelope>> tidy)
|
||||
{
|
||||
super(Kind.FRAMED, channel, response, request, tidy);
|
||||
this.allocator = allocator;
|
||||
}
|
||||
}
|
||||
|
||||
static class Unframed extends FlushItem<Response>
|
||||
{
|
||||
Unframed(Channel channel, Response response, Envelope request, Consumer<FlushItem<Response>> tidy)
|
||||
{
|
||||
super(Kind.UNFRAMED, channel, response, request, tidy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Flusher legacy(EventLoop loop)
|
||||
{
|
||||
return new LegacyFlusher(loop);
|
||||
}
|
||||
|
||||
static Flusher immediate(EventLoop loop)
|
||||
{
|
||||
return new ImmediateFlusher(loop);
|
||||
}
|
||||
|
||||
protected final EventLoop eventLoop;
|
||||
private final ConcurrentLinkedQueue<FlushItem<?>> queued = new ConcurrentLinkedQueue<>();
|
||||
protected final AtomicBoolean scheduled = new AtomicBoolean(false);
|
||||
protected final List<FlushItem<?>> processed = new ArrayList<>();
|
||||
private final HashSet<Channel> channels = new HashSet<>();
|
||||
private final Map<Channel, FlushBuffer> payloads = new HashMap<>();
|
||||
|
||||
void start()
|
||||
{
|
||||
if (!scheduled.get() && scheduled.compareAndSet(false, true))
|
||||
{
|
||||
this.eventLoop.execute(this);
|
||||
}
|
||||
}
|
||||
|
||||
private Flusher(EventLoop eventLoop)
|
||||
{
|
||||
this.eventLoop = eventLoop;
|
||||
}
|
||||
|
||||
void enqueue(FlushItem<?> item)
|
||||
{
|
||||
queued.add(item);
|
||||
}
|
||||
|
||||
FlushItem<?> poll()
|
||||
{
|
||||
return queued.poll();
|
||||
}
|
||||
|
||||
boolean isEmpty()
|
||||
{
|
||||
return queued.isEmpty();
|
||||
}
|
||||
|
||||
private void processUnframedResponse(FlushItem.Unframed flush)
|
||||
{
|
||||
flush.channel.write(flush.response, flush.channel.voidPromise());
|
||||
channels.add(flush.channel);
|
||||
}
|
||||
|
||||
private void processFramedResponse(FlushItem.Framed flush)
|
||||
{
|
||||
Envelope outbound = flush.response;
|
||||
if (envelopeSize(outbound.header) >= MAX_FRAMED_PAYLOAD_SIZE)
|
||||
{
|
||||
flushLargeMessage(flush.channel, outbound, flush.allocator);
|
||||
}
|
||||
else
|
||||
{
|
||||
payloads.computeIfAbsent(flush.channel, channel -> new FlushBuffer(channel, flush.allocator, 5))
|
||||
.add(flush.response);
|
||||
}
|
||||
}
|
||||
|
||||
private void flushLargeMessage(Channel channel, Envelope outbound, FrameEncoder.PayloadAllocator allocator)
|
||||
{
|
||||
FrameEncoder.Payload payload;
|
||||
ByteBuffer buf;
|
||||
ByteBuf body = outbound.body;
|
||||
boolean firstFrame = true;
|
||||
// Highly unlikely that the body of a large message would be empty, but the check is cheap
|
||||
while (body.readableBytes() > 0 || firstFrame)
|
||||
{
|
||||
int payloadSize = Math.min(body.readableBytes(), MAX_FRAMED_PAYLOAD_SIZE);
|
||||
payload = allocator.allocate(false, payloadSize);
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("Allocated initial buffer of {} for 1 large item",
|
||||
FBUtilities.prettyPrintMemory(payload.buffer.capacity()));
|
||||
}
|
||||
|
||||
buf = payload.buffer;
|
||||
// BufferPool may give us a buffer larger than we asked for.
|
||||
// FrameEncoder may object if buffer.remaining is >= MAX_SIZE.
|
||||
if (payloadSize >= MAX_FRAMED_PAYLOAD_SIZE)
|
||||
buf.limit(MAX_FRAMED_PAYLOAD_SIZE);
|
||||
|
||||
if (firstFrame)
|
||||
{
|
||||
outbound.encodeHeaderInto(buf);
|
||||
firstFrame = false;
|
||||
}
|
||||
|
||||
int remaining = Math.min(buf.remaining(), body.readableBytes());
|
||||
if (remaining > 0)
|
||||
buf.put(body.slice(body.readerIndex(), remaining).nioBuffer());
|
||||
|
||||
body.readerIndex(body.readerIndex() + remaining);
|
||||
writeAndFlush(channel, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeAndFlush(Channel channel, FrameEncoder.Payload payload)
|
||||
{
|
||||
// we finish, but not "release" here since we're passing the buffer ownership to FrameEncoder#encode
|
||||
payload.finish();
|
||||
channel.writeAndFlush(payload, channel.voidPromise());
|
||||
}
|
||||
|
||||
protected boolean processQueue()
|
||||
{
|
||||
boolean doneWork = false;
|
||||
FlushItem<?> flush;
|
||||
while ((flush = poll()) != null)
|
||||
{
|
||||
if (flush.kind == FlushItem.Kind.FRAMED)
|
||||
processFramedResponse((FlushItem.Framed) flush);
|
||||
else
|
||||
processUnframedResponse((FlushItem.Unframed) flush);
|
||||
|
||||
processed.add(flush);
|
||||
doneWork = true;
|
||||
}
|
||||
return doneWork;
|
||||
}
|
||||
|
||||
protected void flushWrittenChannels()
|
||||
{
|
||||
// flush the channels pre-V5 to which messages were written in writeSingleResponse
|
||||
for (Channel channel : channels)
|
||||
channel.flush();
|
||||
|
||||
// Framed messages (V5) are grouped by channel, now encode them into payloads, write and flush
|
||||
for (FlushBuffer buffer : payloads.values())
|
||||
buffer.finish();
|
||||
|
||||
// Ultimately, this passes the flush item to the Consumer<FlushItem> configured in
|
||||
// whichever Dispatcher.FlushItemConverter implementation created it. Due to the quite
|
||||
// different ways in which resource allocation is handled in protocol V5 and later
|
||||
// there are distinct implementations for V5 and pre-V5 connections:
|
||||
// * o.a.c.t.CQLMessageHandler::toFlushItem for V5, which relates to FlushItem.Framed.
|
||||
// * o.a.c.t.PreV5Handlers.LegacyDispatchHandler::toFlushItem, relating to FlushItem.Unframed
|
||||
// In both cases, the Consumer releases the buffers for the source envelope and returns the
|
||||
// capacity claimed for message processing back to the global and per-endpoint reserves.
|
||||
// Those reserves are used to determine if capacity is available for any inbound message
|
||||
// or whether we should attempt to shed load or apply backpressure.
|
||||
// The response buffers are handled differently though. In V5, CQL message envelopes are
|
||||
// collated into frames, and so their buffers can be released immediately after flushing.
|
||||
// In V4 however, the buffers containing each CQL envelope are emitted from Envelope.Encoder
|
||||
// and so releasing them is handled by Netty internally.
|
||||
for (FlushItem<?> item : processed)
|
||||
item.release();
|
||||
|
||||
payloads.clear();
|
||||
channels.clear();
|
||||
processed.clear();
|
||||
}
|
||||
|
||||
private class FlushBuffer extends ArrayList<Envelope>
|
||||
{
|
||||
private final Channel channel;
|
||||
private final FrameEncoder.PayloadAllocator allocator;
|
||||
private int sizeInBytes = 0;
|
||||
|
||||
FlushBuffer(Channel channel, FrameEncoder.PayloadAllocator allocator, int initialCapacity)
|
||||
{
|
||||
super(initialCapacity);
|
||||
this.channel = channel;
|
||||
this.allocator = allocator;
|
||||
}
|
||||
|
||||
public boolean add(Envelope toFlush)
|
||||
{
|
||||
sizeInBytes += envelopeSize(toFlush.header);
|
||||
return super.add(toFlush);
|
||||
}
|
||||
|
||||
private FrameEncoder.Payload allocate(int requiredBytes, int maxItems)
|
||||
{
|
||||
int bufferSize = Math.min(requiredBytes, MAX_FRAMED_PAYLOAD_SIZE);
|
||||
FrameEncoder.Payload payload = allocator.allocate(true, bufferSize);
|
||||
// BufferPool may give us a buffer larger than we asked for.
|
||||
// FrameEncoder may object if buffer.remaining is >= MAX_SIZE.
|
||||
if (payload.remaining() >= MAX_FRAMED_PAYLOAD_SIZE)
|
||||
payload.buffer.limit(payload.buffer.position() + bufferSize);
|
||||
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("Allocated initial buffer of {} for up to {} items",
|
||||
FBUtilities.prettyPrintMemory(payload.buffer.capacity()),
|
||||
maxItems);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void finish()
|
||||
{
|
||||
int messageSize;
|
||||
int writtenBytes = 0;
|
||||
int messagesToWrite = this.size();
|
||||
FrameEncoder.Payload sending = allocate(sizeInBytes, messagesToWrite);
|
||||
for (Envelope f : this)
|
||||
{
|
||||
messageSize = envelopeSize(f.header);
|
||||
if (sending.remaining() < messageSize)
|
||||
{
|
||||
writeAndFlush(channel, sending);
|
||||
sending = allocate(sizeInBytes - writtenBytes, messagesToWrite);
|
||||
}
|
||||
|
||||
f.encodeInto(sending.buffer);
|
||||
writtenBytes += messageSize;
|
||||
messagesToWrite--;
|
||||
}
|
||||
writeAndFlush(channel, sending);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LegacyFlusher extends Flusher
|
||||
{
|
||||
int runsSinceFlush = 0;
|
||||
int runsWithNoWork = 0;
|
||||
|
||||
private LegacyFlusher(EventLoop eventLoop)
|
||||
{
|
||||
super(eventLoop);
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
boolean doneWork = processQueue();
|
||||
runsSinceFlush++;
|
||||
|
||||
if (!doneWork || runsSinceFlush > 2 || processed.size() > 50)
|
||||
{
|
||||
flushWrittenChannels();
|
||||
runsSinceFlush = 0;
|
||||
}
|
||||
|
||||
if (doneWork)
|
||||
{
|
||||
runsWithNoWork = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// either reschedule or cancel
|
||||
if (++runsWithNoWork > 5)
|
||||
{
|
||||
scheduled.set(false);
|
||||
if (isEmpty() || !scheduled.compareAndSet(false, true))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
eventLoop.schedule(this, 10000, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ImmediateFlusher extends Flusher
|
||||
{
|
||||
private ImmediateFlusher(EventLoop eventLoop)
|
||||
{
|
||||
super(eventLoop);
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
scheduled.set(false);
|
||||
try
|
||||
{
|
||||
processQueue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
flushWrittenChannels();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.channel.VoidChannelPromise;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.util.Attribute;
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.net.AsyncChannelPromise;
|
||||
import org.apache.cassandra.transport.messages.ErrorMessage;
|
||||
import org.apache.cassandra.transport.messages.StartupMessage;
|
||||
import org.apache.cassandra.transport.messages.SupportedMessage;
|
||||
|
||||
/**
|
||||
* Added to the Netty pipeline whenever a new Channel is initialized. This handler only processes
|
||||
* the messages which constitute the initial handshake between client and server, namely
|
||||
* OPTIONS and STARTUP. After receiving a STARTUP message, the pipeline is reconfigured according
|
||||
* to the protocol version which was negotiated. That reconfiguration should include removing this
|
||||
* handler from the pipeline.
|
||||
*/
|
||||
public class InitialConnectionHandler extends ByteToMessageDecoder
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(InitialConnectionHandler.class);
|
||||
|
||||
final Envelope.Decoder decoder;
|
||||
final Connection.Factory factory;
|
||||
final PipelineConfigurator configurator;
|
||||
|
||||
InitialConnectionHandler(Envelope.Decoder decoder, Connection.Factory factory, PipelineConfigurator configurator)
|
||||
{
|
||||
this.decoder = decoder;
|
||||
this.factory = factory;
|
||||
this.configurator = configurator;
|
||||
}
|
||||
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> list) throws Exception
|
||||
{
|
||||
Envelope inbound = decoder.decode(buffer);
|
||||
if (inbound == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Envelope outbound;
|
||||
switch (inbound.header.type)
|
||||
{
|
||||
case OPTIONS:
|
||||
logger.debug("OPTIONS received {}", inbound.header.version);
|
||||
List<String> cqlVersions = new ArrayList<>();
|
||||
cqlVersions.add(QueryProcessor.CQL_VERSION.toString());
|
||||
|
||||
List<String> compressions = new ArrayList<>();
|
||||
if (Compressor.SnappyCompressor.instance != null)
|
||||
compressions.add("snappy");
|
||||
// LZ4 is always available since worst case scenario it default to a pure JAVA implem.
|
||||
compressions.add("lz4");
|
||||
|
||||
Map<String, List<String>> supportedOptions = new HashMap<>();
|
||||
supportedOptions.put(StartupMessage.CQL_VERSION, cqlVersions);
|
||||
supportedOptions.put(StartupMessage.COMPRESSION, compressions);
|
||||
supportedOptions.put(StartupMessage.PROTOCOL_VERSIONS, ProtocolVersion.supportedVersions());
|
||||
SupportedMessage supported = new SupportedMessage(supportedOptions);
|
||||
outbound = supported.encode(inbound.header.version);
|
||||
ctx.writeAndFlush(outbound);
|
||||
break;
|
||||
|
||||
case STARTUP:
|
||||
Attribute<Connection> attrConn = ctx.channel().attr(Connection.attributeKey);
|
||||
Connection connection = attrConn.get();
|
||||
if (connection == null)
|
||||
{
|
||||
connection = factory.newConnection(ctx.channel(), inbound.header.version);
|
||||
attrConn.set(connection);
|
||||
}
|
||||
assert connection instanceof ServerConnection;
|
||||
|
||||
StartupMessage startup = (StartupMessage) Message.Decoder.decodeMessage(ctx.channel(), inbound);
|
||||
InetAddress remoteAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress();
|
||||
final ClientResourceLimits.Allocator allocator = ClientResourceLimits.getAllocatorForEndpoint(remoteAddress);
|
||||
|
||||
ChannelPromise promise;
|
||||
if (inbound.header.version.isGreaterOrEqualTo(ProtocolVersion.V5))
|
||||
{
|
||||
// in this case we need to defer configuring the pipeline until after the response
|
||||
// has been sent, as the frame encoding specified in v5 should not be applied to
|
||||
// the STARTUP response.
|
||||
allocator.allocate(inbound.header.bodySizeInBytes);
|
||||
promise = AsyncChannelPromise.withListener(ctx, future -> {
|
||||
if (future.isSuccess())
|
||||
{
|
||||
logger.debug("Response to STARTUP sent, configuring pipeline for {}", inbound.header.version);
|
||||
configurator.configureModernPipeline(ctx, allocator, inbound.header.version, startup.options);
|
||||
allocator.release(inbound.header.bodySizeInBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
Throwable cause = future.cause();
|
||||
if (null == cause)
|
||||
cause = new ServerError("Unexpected error establishing connection");
|
||||
logger.warn("Writing response to STARTUP failed, unable to configure pipeline", cause);
|
||||
ErrorMessage error = ErrorMessage.fromException(cause);
|
||||
Envelope response = error.encode(inbound.header.version);
|
||||
ChannelPromise closeChannel = AsyncChannelPromise.withListener(ctx, f -> ctx.close());
|
||||
ctx.writeAndFlush(response, closeChannel);
|
||||
if (ctx.channel().isOpen())
|
||||
ctx.channel().close();
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// no need to configure the pipeline asynchronously in this case
|
||||
// the capacity obtained from allocator for the STARTUP message
|
||||
// is released when flushed by the legacy dispatcher/flusher so
|
||||
// there's no need to explicitly release that here either.
|
||||
configurator.configureLegacyPipeline(ctx, allocator);
|
||||
promise = new VoidChannelPromise(ctx.channel(), false);
|
||||
}
|
||||
|
||||
final Message.Response response = Dispatcher.processRequest((ServerConnection) connection, startup);
|
||||
outbound = response.encode(inbound.header.version);
|
||||
ctx.writeAndFlush(outbound, promise);
|
||||
logger.debug("Configured pipeline: {}", ctx.pipeline());
|
||||
break;
|
||||
|
||||
default:
|
||||
ErrorMessage error =
|
||||
ErrorMessage.fromException(
|
||||
new ProtocolException(String.format("Unexpected message %s, expecting STARTUP or OPTIONS",
|
||||
inbound.header.type)));
|
||||
outbound = error.encode(inbound.header.version);
|
||||
ctx.writeAndFlush(outbound);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
inbound.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,46 +17,28 @@
|
|||
*/
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.concurrent.LocalAwareExecutorService;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.exceptions.OverloadedException;
|
||||
import org.apache.cassandra.metrics.ClientMetrics;
|
||||
import org.apache.cassandra.net.ResourceLimits;
|
||||
import org.apache.cassandra.service.ClientWarn;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.tracing.Tracing;
|
||||
import org.apache.cassandra.transport.messages.*;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
import org.apache.cassandra.utils.UUIDGen;
|
||||
|
||||
import static org.apache.cassandra.concurrent.SharedExecutorPool.SHARED;
|
||||
|
||||
/**
|
||||
* A message from the CQL binary protocol.
|
||||
*/
|
||||
|
|
@ -64,17 +46,6 @@ public abstract class Message
|
|||
{
|
||||
protected static final Logger logger = LoggerFactory.getLogger(Message.class);
|
||||
|
||||
/**
|
||||
* When we encounter an unexpected IOException we look for these {@link Throwable#getMessage() messages}
|
||||
* (because we have no better way to distinguish) and log them at DEBUG rather than INFO, since they
|
||||
* are generally caused by unclean client disconnects rather than an actual problem.
|
||||
*/
|
||||
private static final Set<String> ioExceptionsAtDebugLevel = ImmutableSet.<String>builder().
|
||||
add("Connection reset by peer").
|
||||
add("Broken pipe").
|
||||
add("Connection timed out").
|
||||
build();
|
||||
|
||||
public interface Codec<M extends Message> extends CBCodec<M> {}
|
||||
|
||||
public enum Direction
|
||||
|
|
@ -153,12 +124,25 @@ public abstract class Message
|
|||
t));
|
||||
return t;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Codec<?> unsafeSetCodec(Codec<?> codec) throws NoSuchFieldException, IllegalAccessException
|
||||
{
|
||||
Codec<?> original = this.codec;
|
||||
Field field = Type.class.getDeclaredField("codec");
|
||||
field.setAccessible(true);
|
||||
Field modifiers = Field.class.getDeclaredField("modifiers");
|
||||
modifiers.setAccessible(true);
|
||||
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
|
||||
field.set(this, codec);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
public final Type type;
|
||||
protected Connection connection;
|
||||
private int streamId;
|
||||
private Frame sourceFrame;
|
||||
private Envelope source;
|
||||
private Map<String, ByteBuffer> customPayload;
|
||||
protected ProtocolVersion forcedProtocolVersion = null;
|
||||
|
||||
|
|
@ -188,14 +172,14 @@ public abstract class Message
|
|||
return streamId;
|
||||
}
|
||||
|
||||
public void setSourceFrame(Frame sourceFrame)
|
||||
public void setSource(Envelope source)
|
||||
{
|
||||
this.sourceFrame = sourceFrame;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public Frame getSourceFrame()
|
||||
public Envelope getSource()
|
||||
{
|
||||
return sourceFrame;
|
||||
return source;
|
||||
}
|
||||
|
||||
public Map<String, ByteBuffer> getCustomPayload()
|
||||
|
|
@ -208,6 +192,11 @@ public abstract class Message
|
|||
this.customPayload = customPayload;
|
||||
}
|
||||
|
||||
public String debugString()
|
||||
{
|
||||
return String.format("(%s:%s:%s)", type, streamId, connection == null ? "null" : connection.getVersion().asInt());
|
||||
}
|
||||
|
||||
public static abstract class Request extends Message
|
||||
{
|
||||
private boolean tracingRequested;
|
||||
|
|
@ -227,7 +216,7 @@ public abstract class Message
|
|||
|
||||
protected abstract Response execute(QueryState queryState, long queryStartNanoTime, boolean traceRequest);
|
||||
|
||||
final Response execute(QueryState queryState, long queryStartNanoTime)
|
||||
public final Response execute(QueryState queryState, long queryStartNanoTime)
|
||||
{
|
||||
boolean shouldTrace = false;
|
||||
UUID tracingSessionId = null;
|
||||
|
|
@ -311,572 +300,173 @@ public abstract class Message
|
|||
}
|
||||
}
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public static class ProtocolDecoder extends MessageToMessageDecoder<Frame>
|
||||
public Envelope encode(ProtocolVersion version)
|
||||
{
|
||||
public void decode(ChannelHandlerContext ctx, Frame frame, List results)
|
||||
EnumSet<Envelope.Header.Flag> flags = EnumSet.noneOf(Envelope.Header.Flag.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Codec<Message> codec = (Codec<Message>)this.type.codec;
|
||||
try
|
||||
{
|
||||
boolean isRequest = frame.header.type.direction == Direction.REQUEST;
|
||||
boolean isTracing = frame.header.flags.contains(Frame.Header.Flag.TRACING);
|
||||
boolean isCustomPayload = frame.header.flags.contains(Frame.Header.Flag.CUSTOM_PAYLOAD);
|
||||
boolean hasWarning = frame.header.flags.contains(Frame.Header.Flag.WARNING);
|
||||
|
||||
UUID tracingId = isRequest || !isTracing ? null : CBUtil.readUUID(frame.body);
|
||||
List<String> warnings = isRequest || !hasWarning ? null : CBUtil.readStringList(frame.body);
|
||||
Map<String, ByteBuffer> customPayload = !isCustomPayload ? null : CBUtil.readBytesMap(frame.body);
|
||||
|
||||
try
|
||||
int messageSize = codec.encodedSize(this, version);
|
||||
ByteBuf body;
|
||||
if (this instanceof Response)
|
||||
{
|
||||
if (isCustomPayload && frame.header.version.isSmallerThan(ProtocolVersion.V4))
|
||||
throw new ProtocolException("Received frame with CUSTOM_PAYLOAD flag for native protocol version < 4");
|
||||
|
||||
Message message = frame.header.type.codec.decode(frame.body, frame.header.version);
|
||||
message.setStreamId(frame.header.streamId);
|
||||
message.setSourceFrame(frame);
|
||||
message.setCustomPayload(customPayload);
|
||||
|
||||
if (isRequest)
|
||||
Response message = (Response)this;
|
||||
UUID tracingId = message.getTracingId();
|
||||
Map<String, ByteBuffer> customPayload = message.getCustomPayload();
|
||||
if (tracingId != null)
|
||||
messageSize += CBUtil.sizeOfUUID(tracingId);
|
||||
List<String> warnings = message.getWarnings();
|
||||
if (warnings != null)
|
||||
{
|
||||
assert message instanceof Request;
|
||||
Request req = (Request)message;
|
||||
Connection connection = ctx.channel().attr(Connection.attributeKey).get();
|
||||
req.attach(connection);
|
||||
if (isTracing)
|
||||
req.setTracingRequested();
|
||||
if (version.isSmallerThan(ProtocolVersion.V4))
|
||||
throw new ProtocolException("Must not send frame with WARNING flag for native protocol version < 4");
|
||||
messageSize += CBUtil.sizeOfStringList(warnings);
|
||||
}
|
||||
else
|
||||
if (customPayload != null)
|
||||
{
|
||||
assert message instanceof Response;
|
||||
if (isTracing)
|
||||
((Response)message).setTracingId(tracingId);
|
||||
if (hasWarning)
|
||||
((Response)message).setWarnings(warnings);
|
||||
if (version.isSmallerThan(ProtocolVersion.V4))
|
||||
throw new ProtocolException("Must not send frame with CUSTOM_PAYLOAD flag for native protocol version < 4");
|
||||
messageSize += CBUtil.sizeOfBytesMap(customPayload);
|
||||
}
|
||||
|
||||
results.add(message);
|
||||
}
|
||||
catch (Throwable ex)
|
||||
{
|
||||
frame.release();
|
||||
// Remember the streamId
|
||||
throw ErrorMessage.wrap(ex, frame.header.streamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public static class ProtocolEncoder extends MessageToMessageEncoder<Message>
|
||||
{
|
||||
public void encode(ChannelHandlerContext ctx, Message message, List results)
|
||||
{
|
||||
Connection connection = ctx.channel().attr(Connection.attributeKey).get();
|
||||
// The only case the connection can be null is when we send the initial STARTUP message (client side thus)
|
||||
ProtocolVersion version = connection == null ? ProtocolVersion.CURRENT : connection.getVersion();
|
||||
EnumSet<Frame.Header.Flag> flags = EnumSet.noneOf(Frame.Header.Flag.class);
|
||||
|
||||
Codec<Message> codec = (Codec<Message>)message.type.codec;
|
||||
try
|
||||
{
|
||||
int messageSize = codec.encodedSize(message, version);
|
||||
ByteBuf body;
|
||||
if (message instanceof Response)
|
||||
body = CBUtil.allocator.buffer(messageSize);
|
||||
if (tracingId != null)
|
||||
{
|
||||
UUID tracingId = ((Response)message).getTracingId();
|
||||
Map<String, ByteBuffer> customPayload = message.getCustomPayload();
|
||||
if (tracingId != null)
|
||||
messageSize += CBUtil.sizeOfUUID(tracingId);
|
||||
List<String> warnings = ((Response)message).getWarnings();
|
||||
if (warnings != null)
|
||||
{
|
||||
if (version.isSmallerThan(ProtocolVersion.V4))
|
||||
throw new ProtocolException("Must not send frame with WARNING flag for native protocol version < 4");
|
||||
messageSize += CBUtil.sizeOfStringList(warnings);
|
||||
}
|
||||
if (customPayload != null)
|
||||
{
|
||||
if (version.isSmallerThan(ProtocolVersion.V4))
|
||||
throw new ProtocolException("Must not send frame with CUSTOM_PAYLOAD flag for native protocol version < 4");
|
||||
messageSize += CBUtil.sizeOfBytesMap(customPayload);
|
||||
}
|
||||
body = CBUtil.allocator.buffer(messageSize);
|
||||
if (tracingId != null)
|
||||
{
|
||||
CBUtil.writeUUID(tracingId, body);
|
||||
flags.add(Frame.Header.Flag.TRACING);
|
||||
}
|
||||
if (warnings != null)
|
||||
{
|
||||
CBUtil.writeStringList(warnings, body);
|
||||
flags.add(Frame.Header.Flag.WARNING);
|
||||
}
|
||||
if (customPayload != null)
|
||||
{
|
||||
CBUtil.writeBytesMap(customPayload, body);
|
||||
flags.add(Frame.Header.Flag.CUSTOM_PAYLOAD);
|
||||
}
|
||||
CBUtil.writeUUID(tracingId, body);
|
||||
flags.add(Envelope.Header.Flag.TRACING);
|
||||
}
|
||||
else
|
||||
if (warnings != null)
|
||||
{
|
||||
assert message instanceof Request;
|
||||
if (((Request)message).isTracingRequested())
|
||||
flags.add(Frame.Header.Flag.TRACING);
|
||||
Map<String, ByteBuffer> payload = message.getCustomPayload();
|
||||
if (payload != null)
|
||||
messageSize += CBUtil.sizeOfBytesMap(payload);
|
||||
body = CBUtil.allocator.buffer(messageSize);
|
||||
if (payload != null)
|
||||
{
|
||||
CBUtil.writeBytesMap(payload, body);
|
||||
flags.add(Frame.Header.Flag.CUSTOM_PAYLOAD);
|
||||
}
|
||||
CBUtil.writeStringList(warnings, body);
|
||||
flags.add(Envelope.Header.Flag.WARNING);
|
||||
}
|
||||
|
||||
try
|
||||
if (customPayload != null)
|
||||
{
|
||||
codec.encode(message, body, version);
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
body.release();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// if the driver attempted to connect with a protocol version lower than the minimum supported
|
||||
// version, respond with a protocol error message with the correct frame header for that version
|
||||
ProtocolVersion responseVersion = message.forcedProtocolVersion == null
|
||||
? version
|
||||
: message.forcedProtocolVersion;
|
||||
|
||||
if (responseVersion.isBeta())
|
||||
flags.add(Frame.Header.Flag.USE_BETA);
|
||||
|
||||
results.add(Frame.create(message.type, message.getStreamId(), responseVersion, flags, body));
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
throw ErrorMessage.wrap(e, message.getStreamId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Dispatcher extends SimpleChannelInboundHandler<Request>
|
||||
{
|
||||
private static final LocalAwareExecutorService requestExecutor = SHARED.newExecutor(DatabaseDescriptor.getNativeTransportMaxThreads(),
|
||||
DatabaseDescriptor::setNativeTransportMaxThreads,
|
||||
"transport",
|
||||
"Native-Transport-Requests");
|
||||
|
||||
/**
|
||||
* Current count of *request* bytes that are live on the channel.
|
||||
*
|
||||
* Note: should only be accessed while on the netty event loop.
|
||||
*/
|
||||
private long channelPayloadBytesInFlight;
|
||||
|
||||
private final Server.EndpointPayloadTracker endpointPayloadTracker;
|
||||
|
||||
private boolean paused;
|
||||
|
||||
private static class FlushItem
|
||||
{
|
||||
final ChannelHandlerContext ctx;
|
||||
final Object response;
|
||||
final Frame sourceFrame;
|
||||
final Dispatcher dispatcher;
|
||||
|
||||
private FlushItem(ChannelHandlerContext ctx, Object response, Frame sourceFrame, Dispatcher dispatcher)
|
||||
{
|
||||
this.ctx = ctx;
|
||||
this.sourceFrame = sourceFrame;
|
||||
this.response = response;
|
||||
this.dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
public void release()
|
||||
{
|
||||
dispatcher.releaseItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class Flusher implements Runnable
|
||||
{
|
||||
final EventLoop eventLoop;
|
||||
final ConcurrentLinkedQueue<FlushItem> queued = new ConcurrentLinkedQueue<>();
|
||||
final AtomicBoolean scheduled = new AtomicBoolean(false);
|
||||
final HashSet<ChannelHandlerContext> channels = new HashSet<>();
|
||||
final List<FlushItem> flushed = new ArrayList<>();
|
||||
|
||||
void start()
|
||||
{
|
||||
if (!scheduled.get() && scheduled.compareAndSet(false, true))
|
||||
{
|
||||
this.eventLoop.execute(this);
|
||||
}
|
||||
}
|
||||
|
||||
public Flusher(EventLoop eventLoop)
|
||||
{
|
||||
this.eventLoop = eventLoop;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LegacyFlusher extends Flusher
|
||||
{
|
||||
int runsSinceFlush = 0;
|
||||
int runsWithNoWork = 0;
|
||||
|
||||
private LegacyFlusher(EventLoop eventLoop)
|
||||
{
|
||||
super(eventLoop);
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
|
||||
boolean doneWork = false;
|
||||
FlushItem flush;
|
||||
while ( null != (flush = queued.poll()) )
|
||||
{
|
||||
channels.add(flush.ctx);
|
||||
flush.ctx.write(flush.response, flush.ctx.voidPromise());
|
||||
flushed.add(flush);
|
||||
doneWork = true;
|
||||
}
|
||||
|
||||
runsSinceFlush++;
|
||||
|
||||
if (!doneWork || runsSinceFlush > 2 || flushed.size() > 50)
|
||||
{
|
||||
for (ChannelHandlerContext channel : channels)
|
||||
channel.flush();
|
||||
for (FlushItem item : flushed)
|
||||
item.release();
|
||||
|
||||
channels.clear();
|
||||
flushed.clear();
|
||||
runsSinceFlush = 0;
|
||||
}
|
||||
|
||||
if (doneWork)
|
||||
{
|
||||
runsWithNoWork = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// either reschedule or cancel
|
||||
if (++runsWithNoWork > 5)
|
||||
{
|
||||
scheduled.set(false);
|
||||
if (queued.isEmpty() || !scheduled.compareAndSet(false, true))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
eventLoop.schedule(this, 10000, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ImmediateFlusher extends Flusher
|
||||
{
|
||||
private ImmediateFlusher(EventLoop eventLoop)
|
||||
{
|
||||
super(eventLoop);
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
boolean doneWork = false;
|
||||
FlushItem flush;
|
||||
scheduled.set(false);
|
||||
|
||||
while (null != (flush = queued.poll()))
|
||||
{
|
||||
channels.add(flush.ctx);
|
||||
flush.ctx.write(flush.response, flush.ctx.voidPromise());
|
||||
flushed.add(flush);
|
||||
doneWork = true;
|
||||
}
|
||||
|
||||
if (doneWork)
|
||||
{
|
||||
for (ChannelHandlerContext channel : channels)
|
||||
channel.flush();
|
||||
for (FlushItem item : flushed)
|
||||
item.release();
|
||||
|
||||
channels.clear();
|
||||
flushed.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final ConcurrentMap<EventLoop, Flusher> flusherLookup = new ConcurrentHashMap<>();
|
||||
|
||||
private final boolean useLegacyFlusher;
|
||||
|
||||
public Dispatcher(boolean useLegacyFlusher, Server.EndpointPayloadTracker endpointPayloadTracker)
|
||||
{
|
||||
super(false);
|
||||
this.useLegacyFlusher = useLegacyFlusher;
|
||||
this.endpointPayloadTracker = endpointPayloadTracker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead0(ChannelHandlerContext ctx, Request request)
|
||||
{
|
||||
// if we decide to handle this message, process it outside of the netty event loop
|
||||
if (shouldHandleRequest(ctx, request))
|
||||
requestExecutor.submit(() -> processRequest(ctx, request));
|
||||
}
|
||||
|
||||
/** This check for inflight payload to potentially discard the request should have been ideally in one of the
|
||||
* first handlers in the pipeline (Frame::decode()). However, incase of any exception thrown between that
|
||||
* handler (where inflight payload is incremented) and this handler (Dispatcher::channelRead0) (where inflight
|
||||
* payload in decremented), inflight payload becomes erroneous. ExceptionHandler is not sufficient for this
|
||||
* purpose since it does not have the frame associated with the exception.
|
||||
*
|
||||
* Note: this method should execute on the netty event loop.
|
||||
*/
|
||||
private boolean shouldHandleRequest(ChannelHandlerContext ctx, Request request)
|
||||
{
|
||||
long frameSize = request.getSourceFrame().header.bodySizeInBytes;
|
||||
|
||||
ResourceLimits.EndpointAndGlobal endpointAndGlobalPayloadsInFlight = endpointPayloadTracker.endpointAndGlobalPayloadsInFlight;
|
||||
|
||||
// check for overloaded state by trying to allocate framesize to inflight payload trackers
|
||||
if (endpointAndGlobalPayloadsInFlight.tryAllocate(frameSize) != ResourceLimits.Outcome.SUCCESS)
|
||||
{
|
||||
if (request.connection.isThrowOnOverload())
|
||||
{
|
||||
// discard the request and throw an exception
|
||||
ClientMetrics.instance.markRequestDiscarded();
|
||||
logger.trace("Discarded request of size: {}. InflightChannelRequestPayload: {}, InflightEndpointRequestPayload: {}, InflightOverallRequestPayload: {}, Request: {}",
|
||||
frameSize,
|
||||
channelPayloadBytesInFlight,
|
||||
endpointAndGlobalPayloadsInFlight.endpoint().using(),
|
||||
endpointAndGlobalPayloadsInFlight.global().using(),
|
||||
request);
|
||||
throw ErrorMessage.wrap(new OverloadedException("Server is in overloaded state. Cannot accept more requests at this point"),
|
||||
request.getSourceFrame().header.streamId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set backpressure on the channel, and handle the request
|
||||
endpointAndGlobalPayloadsInFlight.allocate(frameSize);
|
||||
ctx.channel().config().setAutoRead(false);
|
||||
ClientMetrics.instance.pauseConnection();
|
||||
paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
channelPayloadBytesInFlight += frameSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this method will be used in the {@link Flusher#run()}, which executes on the netty event loop
|
||||
* ({@link Dispatcher#flusherLookup}). Thus, we assume the semantics and visibility of variables
|
||||
* of being on the event loop.
|
||||
*/
|
||||
private void releaseItem(FlushItem item)
|
||||
{
|
||||
long itemSize = item.sourceFrame.header.bodySizeInBytes;
|
||||
item.sourceFrame.release();
|
||||
|
||||
// since the request has been processed, decrement inflight payload at channel, endpoint and global levels
|
||||
channelPayloadBytesInFlight -= itemSize;
|
||||
ResourceLimits.Outcome endpointGlobalReleaseOutcome = endpointPayloadTracker.endpointAndGlobalPayloadsInFlight.release(itemSize);
|
||||
|
||||
// now check to see if we need to reenable the channel's autoRead.
|
||||
// If the current payload side is zero, we must reenable autoread as
|
||||
// 1) we allow no other thread/channel to do it, and
|
||||
// 2) there's no other events following this one (becuase we're at zero bytes in flight),
|
||||
// so no successive to trigger the other clause in this if-block
|
||||
ChannelConfig config = item.ctx.channel().config();
|
||||
if (paused && (channelPayloadBytesInFlight == 0 || endpointGlobalReleaseOutcome == ResourceLimits.Outcome.BELOW_LIMIT))
|
||||
{
|
||||
paused = false;
|
||||
ClientMetrics.instance.unpauseConnection();
|
||||
config.setAutoRead(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this method is not expected to execute on the netty event loop.
|
||||
*/
|
||||
void processRequest(ChannelHandlerContext ctx, Request request)
|
||||
{
|
||||
final Response response;
|
||||
final ServerConnection connection;
|
||||
long queryStartNanoTime = System.nanoTime();
|
||||
|
||||
try
|
||||
{
|
||||
assert request.connection() instanceof ServerConnection;
|
||||
connection = (ServerConnection)request.connection();
|
||||
if (connection.getVersion().isGreaterOrEqualTo(ProtocolVersion.V4))
|
||||
ClientWarn.instance.captureWarnings();
|
||||
|
||||
QueryState qstate = connection.validateNewMessage(request.type, connection.getVersion());
|
||||
|
||||
logger.trace("Received: {}, v={}", request, connection.getVersion());
|
||||
connection.requests.inc();
|
||||
response = request.execute(qstate, queryStartNanoTime);
|
||||
response.setStreamId(request.getStreamId());
|
||||
response.setWarnings(ClientWarn.instance.getWarnings());
|
||||
response.attach(connection);
|
||||
connection.applyStateTransition(request.type, response.type);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
JVMStabilityInspector.inspectThrowable(t);
|
||||
UnexpectedChannelExceptionHandler handler = new UnexpectedChannelExceptionHandler(ctx.channel(), true);
|
||||
flush(new FlushItem(ctx, ErrorMessage.fromException(t, handler).setStreamId(request.getStreamId()), request.getSourceFrame(), this));
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ClientWarn.instance.resetWarnings();
|
||||
}
|
||||
|
||||
logger.trace("Responding: {}, v={}", response, connection.getVersion());
|
||||
flush(new FlushItem(ctx, response, request.getSourceFrame(), this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx)
|
||||
{
|
||||
endpointPayloadTracker.release();
|
||||
if (paused)
|
||||
{
|
||||
paused = false;
|
||||
ClientMetrics.instance.unpauseConnection();
|
||||
}
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
private void flush(FlushItem item)
|
||||
{
|
||||
EventLoop loop = item.ctx.channel().eventLoop();
|
||||
Flusher flusher = flusherLookup.get(loop);
|
||||
if (flusher == null)
|
||||
{
|
||||
Flusher created = useLegacyFlusher ? new LegacyFlusher(loop) : new ImmediateFlusher(loop);
|
||||
Flusher alt = flusherLookup.putIfAbsent(loop, flusher = created);
|
||||
if (alt != null)
|
||||
flusher = alt;
|
||||
}
|
||||
|
||||
flusher.queued.add(item);
|
||||
flusher.start();
|
||||
}
|
||||
|
||||
public static void shutdown()
|
||||
{
|
||||
if (requestExecutor != null)
|
||||
{
|
||||
requestExecutor.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public static final class ExceptionHandler extends ChannelInboundHandlerAdapter
|
||||
{
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final ChannelHandlerContext ctx, Throwable cause)
|
||||
{
|
||||
// Provide error message to client in case channel is still open
|
||||
UnexpectedChannelExceptionHandler handler = new UnexpectedChannelExceptionHandler(ctx.channel(), false);
|
||||
ErrorMessage errorMessage = ErrorMessage.fromException(cause, handler);
|
||||
if (ctx.channel().isOpen())
|
||||
{
|
||||
ChannelFuture future = ctx.writeAndFlush(errorMessage);
|
||||
// On protocol exception, close the channel as soon as the message have been sent
|
||||
if (cause instanceof ProtocolException)
|
||||
{
|
||||
future.addListener(new ChannelFutureListener()
|
||||
{
|
||||
public void operationComplete(ChannelFuture future)
|
||||
{
|
||||
ctx.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
JVMStabilityInspector.inspectThrowable(cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the channel info in the logged information for unexpected errors, and (if {@link #alwaysLogAtError} is
|
||||
* false then choose the log level based on the type of exception (some are clearly client issues and shouldn't be
|
||||
* logged at server ERROR level)
|
||||
*/
|
||||
static final class UnexpectedChannelExceptionHandler implements Predicate<Throwable>
|
||||
{
|
||||
private final Channel channel;
|
||||
private final boolean alwaysLogAtError;
|
||||
|
||||
UnexpectedChannelExceptionHandler(Channel channel, boolean alwaysLogAtError)
|
||||
{
|
||||
this.channel = channel;
|
||||
this.alwaysLogAtError = alwaysLogAtError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Throwable exception)
|
||||
{
|
||||
String message;
|
||||
try
|
||||
{
|
||||
message = "Unexpected exception during request; channel = " + channel;
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
// We don't want to make things worse if String.valueOf() throws an exception
|
||||
message = "Unexpected exception during request; channel = <unprintable>";
|
||||
}
|
||||
|
||||
// netty wraps SSL errors in a CodecExcpetion
|
||||
boolean isIOException = exception instanceof IOException || (exception.getCause() instanceof IOException);
|
||||
if (!alwaysLogAtError && isIOException)
|
||||
{
|
||||
String errorMessage = exception.getMessage();
|
||||
boolean logAtTrace = false;
|
||||
|
||||
for (String ioException : ioExceptionsAtDebugLevel)
|
||||
{
|
||||
// exceptions thrown from the netty epoll transport add the name of the function that failed
|
||||
// to the exception string (which is simply wrapping a JDK exception), so we can't do a simple/naive comparison
|
||||
if (errorMessage.contains(ioException))
|
||||
{
|
||||
logAtTrace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (logAtTrace)
|
||||
{
|
||||
// Likely unclean client disconnects
|
||||
logger.trace(message, exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generally unhandled IO exceptions are network issues, not actual ERRORS
|
||||
logger.info(message, exception);
|
||||
CBUtil.writeBytesMap(customPayload, body);
|
||||
flags.add(Envelope.Header.Flag.CUSTOM_PAYLOAD);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Anything else is probably a bug in server of client binary protocol handling
|
||||
logger.error(message, exception);
|
||||
assert this instanceof Request;
|
||||
if (((Request)this).isTracingRequested())
|
||||
flags.add(Envelope.Header.Flag.TRACING);
|
||||
Map<String, ByteBuffer> payload = getCustomPayload();
|
||||
if (payload != null)
|
||||
messageSize += CBUtil.sizeOfBytesMap(payload);
|
||||
body = CBUtil.allocator.buffer(messageSize);
|
||||
if (payload != null)
|
||||
{
|
||||
CBUtil.writeBytesMap(payload, body);
|
||||
flags.add(Envelope.Header.Flag.CUSTOM_PAYLOAD);
|
||||
}
|
||||
}
|
||||
|
||||
// We handled the exception.
|
||||
return true;
|
||||
try
|
||||
{
|
||||
codec.encode(this, body, version);
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
body.release();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// if the driver attempted to connect with a protocol version lower than the minimum supported
|
||||
// version, respond with a protocol error message with the correct message header for that version
|
||||
ProtocolVersion responseVersion = forcedProtocolVersion == null
|
||||
? version
|
||||
: forcedProtocolVersion;
|
||||
|
||||
if (responseVersion.isBeta())
|
||||
flags.add(Envelope.Header.Flag.USE_BETA);
|
||||
|
||||
return Envelope.create(type, getStreamId(), responseVersion, flags, body);
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
throw ErrorMessage.wrap(e, getStreamId());
|
||||
}
|
||||
}
|
||||
|
||||
abstract static class Decoder<M extends Message>
|
||||
{
|
||||
static Message decodeMessage(Channel channel, Envelope inbound)
|
||||
{
|
||||
boolean isRequest = inbound.header.type.direction == Direction.REQUEST;
|
||||
boolean isTracing = inbound.header.flags.contains(Envelope.Header.Flag.TRACING);
|
||||
boolean isCustomPayload = inbound.header.flags.contains(Envelope.Header.Flag.CUSTOM_PAYLOAD);
|
||||
boolean hasWarning = inbound.header.flags.contains(Envelope.Header.Flag.WARNING);
|
||||
|
||||
UUID tracingId = isRequest || !isTracing ? null : CBUtil.readUUID(inbound.body);
|
||||
List<String> warnings = isRequest || !hasWarning ? null : CBUtil.readStringList(inbound.body);
|
||||
Map<String, ByteBuffer> customPayload = !isCustomPayload ? null : CBUtil.readBytesMap(inbound.body);
|
||||
|
||||
if (isCustomPayload && inbound.header.version.isSmallerThan(ProtocolVersion.V4))
|
||||
throw new ProtocolException("Received frame with CUSTOM_PAYLOAD flag for native protocol version < 4");
|
||||
|
||||
Message message = inbound.header.type.codec.decode(inbound.body, inbound.header.version);
|
||||
message.setStreamId(inbound.header.streamId);
|
||||
message.setSource(inbound);
|
||||
message.setCustomPayload(customPayload);
|
||||
|
||||
if (isRequest)
|
||||
{
|
||||
assert message instanceof Request;
|
||||
Request req = (Request) message;
|
||||
Connection connection = channel.attr(Connection.attributeKey).get();
|
||||
req.attach(connection);
|
||||
if (isTracing)
|
||||
req.setTracingRequested();
|
||||
}
|
||||
else
|
||||
{
|
||||
assert message instanceof Response;
|
||||
if (isTracing)
|
||||
((Response) message).setTracingId(tracingId);
|
||||
if (hasWarning)
|
||||
((Response) message).setWarnings(warnings);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
abstract M decode(Channel channel, Envelope inbound);
|
||||
|
||||
private static class RequestDecoder extends Decoder<Request>
|
||||
{
|
||||
Request decode(Channel channel, Envelope request)
|
||||
{
|
||||
if (request.header.type.direction != Direction.REQUEST)
|
||||
throw new ProtocolException(String.format("Unexpected RESPONSE message %s, expecting REQUEST",
|
||||
request.header.type));
|
||||
|
||||
return (Request) decodeMessage(channel, request);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ResponseDecoder extends Decoder<Response>
|
||||
{
|
||||
Response decode(Channel channel, Envelope response)
|
||||
{
|
||||
if (response.header.type.direction != Direction.RESPONSE)
|
||||
throw new ProtocolException(String.format("Unexpected REQUEST message %s, expecting RESPONSE",
|
||||
response.header.type));
|
||||
|
||||
return (Response) decodeMessage(channel, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Decoder.RequestDecoder REQUEST_DECODER = new Decoder.RequestDecoder();
|
||||
private static final Decoder.ResponseDecoder RESPONSE_DECODER = new Decoder.ResponseDecoder();
|
||||
|
||||
static Decoder<Message.Request> requestDecoder()
|
||||
{
|
||||
return REQUEST_DECODER;
|
||||
}
|
||||
|
||||
static Decoder<Message.Response> responseDecoder()
|
||||
{
|
||||
return RESPONSE_DECODER;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,361 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.logging.LoggingHandler;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import io.netty.util.Version;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.net.*;
|
||||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.transport.messages.StartupMessage;
|
||||
|
||||
/**
|
||||
* Takes care of intializing a Netty Channel and Pipeline for client protocol connections.
|
||||
* The pipeline is first set up with some common handlers for connection limiting, dropping
|
||||
* idle connections and optionally SSL, along with a handler to deal with the handshake
|
||||
* between client and server. That handshake handler calls back to this class to reconfigure
|
||||
* the pipeline once the protocol version for the connection has been established.
|
||||
*/
|
||||
public class PipelineConfigurator
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(PipelineConfigurator.class);
|
||||
|
||||
// Not to be used in production, this causes a Netty logging handler to be added to the pipeline,
|
||||
// which will throttle a system under any normal load.
|
||||
private static final boolean DEBUG = Boolean.getBoolean("cassandra.unsafe_verbose_debug_client_protocol");
|
||||
|
||||
// Stateless handlers
|
||||
private static final ConnectionLimitHandler connectionLimitHandler = new ConnectionLimitHandler();
|
||||
|
||||
// Names of handlers used regardless of protocol version
|
||||
private static final String CONNECTION_LIMIT_HANDLER = "connectionLimitHandler";
|
||||
private static final String IDLE_STATE_HANDLER = "idleStateHandler";
|
||||
private static final String INITIAL_HANDLER = "initialHandler";
|
||||
private static final String EXCEPTION_HANDLER = "exceptionHandler";
|
||||
private static final String DEBUG_HANDLER = "debugHandler";
|
||||
private static final String SSL_HANDLER = "ssl";
|
||||
|
||||
// Names of handlers used in pre-V5 pipelines only
|
||||
private static final String ENVELOPE_DECODER = "envelopeDecoder";
|
||||
private static final String ENVELOPE_ENCODER = "envelopeEncoder";
|
||||
private static final String MESSAGE_DECOMPRESSOR = "decompressor";
|
||||
private static final String MESSAGE_COMPRESSOR = "compressor";
|
||||
private static final String MESSAGE_DECODER = "messageDecoder";
|
||||
private static final String MESSAGE_ENCODER = "messageEncoder";
|
||||
private static final String LEGACY_MESSAGE_PROCESSOR = "legacyCqlProcessor";
|
||||
|
||||
// Names of handlers used in V5 and later pipelines
|
||||
private static final String FRAME_DECODER = "frameDecoder";
|
||||
private static final String FRAME_ENCODER = "frameEncoder";
|
||||
private static final String MESSAGE_PROCESSOR = "cqlProcessor";
|
||||
|
||||
private final boolean epoll;
|
||||
private final boolean keepAlive;
|
||||
private final EncryptionOptions.TlsEncryptionPolicy tlsEncryptionPolicy;
|
||||
private final Dispatcher dispatcher;
|
||||
|
||||
public PipelineConfigurator(boolean epoll,
|
||||
boolean keepAlive,
|
||||
boolean legacyFlusher,
|
||||
EncryptionOptions.TlsEncryptionPolicy encryptionPolicy)
|
||||
{
|
||||
this.epoll = epoll;
|
||||
this.keepAlive = keepAlive;
|
||||
this.tlsEncryptionPolicy = encryptionPolicy;
|
||||
this.dispatcher = dispatcher(legacyFlusher);
|
||||
}
|
||||
|
||||
public ChannelFuture initializeChannel(final EventLoopGroup workerGroup,
|
||||
final InetSocketAddress socket,
|
||||
final Connection.Factory connectionFactory)
|
||||
{
|
||||
ServerBootstrap bootstrap = new ServerBootstrap()
|
||||
.channel(epoll ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.childOption(ChannelOption.SO_LINGER, 0)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, keepAlive)
|
||||
.childOption(ChannelOption.ALLOCATOR, CBUtil.allocator)
|
||||
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(8 * 1024, 32 * 1024));
|
||||
if (workerGroup != null)
|
||||
bootstrap = bootstrap.group(workerGroup);
|
||||
|
||||
ChannelInitializer<Channel> initializer = initializer(connectionFactory);
|
||||
bootstrap.childHandler(initializer);
|
||||
|
||||
// Bind and start to accept incoming connections.
|
||||
logger.info("Using Netty Version: {}", Version.identify().entrySet());
|
||||
logger.info("Starting listening for CQL clients on {} ({})...", socket, tlsEncryptionPolicy.description());
|
||||
return bootstrap.bind(socket);
|
||||
}
|
||||
|
||||
protected ChannelInitializer<Channel> initializer(Connection.Factory connectionFactory)
|
||||
{
|
||||
// the initializer will perform the common initial setup
|
||||
// then any additional steps mandated by the encryption options
|
||||
final EncryptionConfig encryptionConfig = encryptionConfig();
|
||||
return new ChannelInitializer<Channel>()
|
||||
{
|
||||
protected void initChannel(Channel channel) throws Exception
|
||||
{
|
||||
configureInitialPipeline(channel, connectionFactory);
|
||||
encryptionConfig.applyTo(channel);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Essentially just a Consumer<Channel> which may throw
|
||||
interface EncryptionConfig
|
||||
{
|
||||
void applyTo(Channel channel) throws Exception;
|
||||
}
|
||||
|
||||
protected EncryptionConfig encryptionConfig()
|
||||
{
|
||||
final EncryptionOptions encryptionOptions = DatabaseDescriptor.getNativeProtocolEncryptionOptions();
|
||||
switch (tlsEncryptionPolicy)
|
||||
{
|
||||
case UNENCRYPTED:
|
||||
// if encryption is not enabled, no further steps are required after the initial setup
|
||||
return channel -> {};
|
||||
case OPTIONAL:
|
||||
// If optional, install a handler which detects whether or not the client is sending
|
||||
// encrypted bytes. If so, on receipt of the next bytes, replace that handler with
|
||||
// an SSL Handler, otherwise just remove it and proceed with an unencrypted channel.
|
||||
logger.debug("Enabling optionally encrypted CQL connections between client and server");
|
||||
return channel -> {
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
||||
encryptionOptions.require_client_auth,
|
||||
SSLFactory.SocketType.SERVER);
|
||||
|
||||
channel.pipeline().addFirst(SSL_HANDLER, new ByteToMessageDecoder()
|
||||
{
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception
|
||||
{
|
||||
if (byteBuf.readableBytes() < 5)
|
||||
{
|
||||
// To detect if SSL must be used we need to have at least 5 bytes, so return here and try again
|
||||
// once more bytes a ready.
|
||||
return;
|
||||
}
|
||||
if (SslHandler.isEncrypted(byteBuf))
|
||||
{
|
||||
// Connection uses SSL/TLS, replace the detection handler with a SslHandler and so use
|
||||
// encryption.
|
||||
SslHandler sslHandler = sslContext.newHandler(channel.alloc());
|
||||
channelHandlerContext.pipeline().replace(SSL_HANDLER, SSL_HANDLER, sslHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Connection use no TLS/SSL encryption, just remove the detection handler and continue without
|
||||
// SslHandler in the pipeline.
|
||||
channelHandlerContext.pipeline().remove(SSL_HANDLER);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
case ENCRYPTED:
|
||||
logger.debug("Enabling encrypted CQL connections between client and server");
|
||||
return channel -> {
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
||||
encryptionOptions.require_client_auth,
|
||||
SSLFactory.SocketType.SERVER);
|
||||
channel.pipeline().addFirst(SSL_HANDLER, sslContext.newHandler(channel.alloc()));
|
||||
};
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized TLS encryption policy: " + this.tlsEncryptionPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
public void configureInitialPipeline(Channel channel, Connection.Factory connectionFactory)
|
||||
{
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
|
||||
// Add the ConnectionLimitHandler to the pipeline if configured to do so.
|
||||
if (DatabaseDescriptor.getNativeTransportMaxConcurrentConnections() > 0
|
||||
|| DatabaseDescriptor.getNativeTransportMaxConcurrentConnectionsPerIp() > 0)
|
||||
{
|
||||
// Add as first to the pipeline so the limit is enforced as first action.
|
||||
pipeline.addFirst(CONNECTION_LIMIT_HANDLER, connectionLimitHandler);
|
||||
}
|
||||
|
||||
long idleTimeout = DatabaseDescriptor.nativeTransportIdleTimeout();
|
||||
if (idleTimeout > 0)
|
||||
{
|
||||
pipeline.addLast(IDLE_STATE_HANDLER, new IdleStateHandler(false, 0, 0, idleTimeout, TimeUnit.MILLISECONDS)
|
||||
{
|
||||
@Override
|
||||
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt)
|
||||
{
|
||||
logger.info("Closing client connection {} after timeout of {}ms", channel.remoteAddress(), idleTimeout);
|
||||
ctx.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (DEBUG)
|
||||
pipeline.addLast(DEBUG_HANDLER, new LoggingHandler(LogLevel.INFO));
|
||||
|
||||
pipeline.addLast(ENVELOPE_ENCODER, Envelope.Encoder.instance);
|
||||
pipeline.addLast(INITIAL_HANDLER, new InitialConnectionHandler(new Envelope.Decoder(), connectionFactory, this));
|
||||
// The exceptionHandler will take care of handling exceptionCaught(...) events while still running
|
||||
// on the same EventLoop as all previous added handlers in the pipeline. This is important as the used
|
||||
// eventExecutorGroup may not enforce strict ordering for channel events.
|
||||
// As the exceptionHandler runs in the EventLoop as the previous handlers we are sure all exceptions are
|
||||
// correctly handled before the handler itself is removed.
|
||||
// See https://issues.apache.org/jira/browse/CASSANDRA-13649
|
||||
pipeline.addLast(EXCEPTION_HANDLER, PreV5Handlers.ExceptionHandler.instance);
|
||||
onInitialPipelineReady(pipeline);
|
||||
}
|
||||
|
||||
public void configureModernPipeline(ChannelHandlerContext ctx,
|
||||
ClientResourceLimits.Allocator resourceAllocator,
|
||||
ProtocolVersion version,
|
||||
Map<String, String> options)
|
||||
{
|
||||
BufferPoolAllocator allocator = GlobalBufferPoolAllocator.instance;
|
||||
ctx.channel().config().setOption(ChannelOption.ALLOCATOR, allocator);
|
||||
|
||||
// Transport level encoders/decoders
|
||||
String compression = options.get(StartupMessage.COMPRESSION);
|
||||
FrameDecoder frameDecoder = frameDecoder(compression, allocator);
|
||||
FrameEncoder frameEncoder = frameEncoder(compression);
|
||||
FrameEncoder.PayloadAllocator payloadAllocator = frameEncoder.allocator();
|
||||
ChannelInboundHandlerAdapter exceptionHandler = ExceptionHandlers.postV5Handler(payloadAllocator, version);
|
||||
|
||||
// CQL level encoders/decoders
|
||||
Message.Decoder<Message.Request> messageDecoder = messageDecoder();
|
||||
Envelope.Decoder envelopeDecoder = new Envelope.Decoder();
|
||||
|
||||
// Any non-fatal errors caught in CQLMessageHandler propagate back to the client
|
||||
// via the pipeline. Firing the exceptionCaught event on an inbound handler context
|
||||
// (in this case, the initial context) will cause it to propagate to to the
|
||||
// exceptionHandler provided none of the the intermediate handlers drop it
|
||||
// in their exceptionCaught implementation
|
||||
ChannelPipeline pipeline = ctx.channel().pipeline();
|
||||
final ChannelHandlerContext firstContext = pipeline.firstContext();
|
||||
CQLMessageHandler.ErrorHandler errorHandler = firstContext::fireExceptionCaught;
|
||||
|
||||
// Capacity tracking and resource management
|
||||
int queueCapacity = DatabaseDescriptor.getNativeTransportReceiveQueueCapacityInBytes();
|
||||
ClientResourceLimits.ResourceProvider resourceProvider = resourceProvider(resourceAllocator);
|
||||
AbstractMessageHandler.OnHandlerClosed onClosed = handler -> resourceProvider.release();
|
||||
boolean throwOnOverload = "1".equals(options.get(StartupMessage.THROW_ON_OVERLOAD));
|
||||
|
||||
CQLMessageHandler.MessageConsumer<Message.Request> messageConsumer = messageConsumer();
|
||||
CQLMessageHandler<Message.Request> processor =
|
||||
new CQLMessageHandler<>(ctx.channel(),
|
||||
frameDecoder,
|
||||
envelopeDecoder,
|
||||
messageDecoder,
|
||||
messageConsumer,
|
||||
payloadAllocator,
|
||||
queueCapacity,
|
||||
resourceProvider,
|
||||
onClosed,
|
||||
errorHandler,
|
||||
throwOnOverload);
|
||||
|
||||
pipeline.remove(ENVELOPE_ENCODER); // remove old outbound cql envelope encoder
|
||||
pipeline.addBefore(INITIAL_HANDLER, FRAME_DECODER, frameDecoder);
|
||||
pipeline.addBefore(INITIAL_HANDLER, FRAME_ENCODER, frameEncoder);
|
||||
pipeline.addBefore(INITIAL_HANDLER, MESSAGE_PROCESSOR, processor);
|
||||
pipeline.replace(EXCEPTION_HANDLER, EXCEPTION_HANDLER, exceptionHandler);
|
||||
pipeline.remove(INITIAL_HANDLER);
|
||||
onNegotiationComplete(pipeline);
|
||||
}
|
||||
|
||||
protected void onInitialPipelineReady(ChannelPipeline pipeline) {}
|
||||
protected void onNegotiationComplete(ChannelPipeline pipeline) {}
|
||||
|
||||
protected ClientResourceLimits.ResourceProvider resourceProvider(ClientResourceLimits.Allocator allocator)
|
||||
{
|
||||
return new ClientResourceLimits.ResourceProvider.Default(allocator);
|
||||
}
|
||||
|
||||
protected Dispatcher dispatcher(boolean useLegacyFlusher)
|
||||
{
|
||||
return new Dispatcher(useLegacyFlusher);
|
||||
}
|
||||
|
||||
protected CQLMessageHandler.MessageConsumer<Message.Request> messageConsumer()
|
||||
{
|
||||
return dispatcher::dispatch;
|
||||
}
|
||||
|
||||
protected Message.Decoder<Message.Request> messageDecoder()
|
||||
{
|
||||
return Message.requestDecoder();
|
||||
}
|
||||
|
||||
protected FrameDecoder frameDecoder(String compression, BufferPoolAllocator allocator)
|
||||
{
|
||||
if (null == compression)
|
||||
return FrameDecoderCrc.create(allocator);
|
||||
if (compression.equalsIgnoreCase("LZ4"))
|
||||
return FrameDecoderLZ4.fast(allocator);
|
||||
throw new ProtocolException("Unsupported compression type: " + compression);
|
||||
}
|
||||
|
||||
protected FrameEncoder frameEncoder(String compression)
|
||||
{
|
||||
if (Strings.isNullOrEmpty(compression))
|
||||
return FrameEncoderCrc.instance;
|
||||
if (compression.equalsIgnoreCase("LZ4"))
|
||||
return FrameEncoderLZ4.fastInstance;
|
||||
throw new ProtocolException("Unsupported compression type: " + compression);
|
||||
}
|
||||
|
||||
public void configureLegacyPipeline(ChannelHandlerContext ctx, ClientResourceLimits.Allocator limits)
|
||||
{
|
||||
ChannelPipeline pipeline = ctx.channel().pipeline();
|
||||
pipeline.addBefore(ENVELOPE_ENCODER, ENVELOPE_DECODER, new Envelope.Decoder());
|
||||
pipeline.addBefore(INITIAL_HANDLER, MESSAGE_DECOMPRESSOR, Envelope.Decompressor.instance);
|
||||
pipeline.addBefore(INITIAL_HANDLER, MESSAGE_COMPRESSOR, Envelope.Compressor.instance);
|
||||
pipeline.addBefore(INITIAL_HANDLER, MESSAGE_DECODER, PreV5Handlers.ProtocolDecoder.instance);
|
||||
pipeline.addBefore(INITIAL_HANDLER, MESSAGE_ENCODER, PreV5Handlers.ProtocolEncoder.instance);
|
||||
pipeline.addBefore(INITIAL_HANDLER, LEGACY_MESSAGE_PROCESSOR, new PreV5Handlers.LegacyDispatchHandler(dispatcher, limits));
|
||||
pipeline.remove(INITIAL_HANDLER);
|
||||
onNegotiationComplete(pipeline);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelConfig;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
import org.apache.cassandra.exceptions.OverloadedException;
|
||||
import org.apache.cassandra.metrics.ClientMetrics;
|
||||
import org.apache.cassandra.net.ResourceLimits;
|
||||
import org.apache.cassandra.transport.messages.ErrorMessage;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
|
||||
public class PreV5Handlers
|
||||
{
|
||||
/**
|
||||
* Wraps an {@link org.apache.cassandra.transport.Dispatcher} so that it can be used as an
|
||||
* channel inbound handler in pre-V5 pipelines.
|
||||
*/
|
||||
public static class LegacyDispatchHandler extends SimpleChannelInboundHandler<Message.Request>
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(LegacyDispatchHandler.class);
|
||||
|
||||
private final Dispatcher dispatcher;
|
||||
private final ClientResourceLimits.Allocator endpointPayloadTracker;
|
||||
|
||||
/**
|
||||
* Current count of *request* bytes that are live on the channel.
|
||||
* <p>
|
||||
* Note: should only be accessed while on the netty event loop.
|
||||
*/
|
||||
private long channelPayloadBytesInFlight;
|
||||
private boolean paused;
|
||||
|
||||
LegacyDispatchHandler(Dispatcher dispatcher, ClientResourceLimits.Allocator endpointPayloadTracker)
|
||||
{
|
||||
this.dispatcher = dispatcher;
|
||||
this.endpointPayloadTracker = endpointPayloadTracker;
|
||||
}
|
||||
|
||||
protected void channelRead0(ChannelHandlerContext ctx, Message.Request request) throws Exception
|
||||
{
|
||||
// if we decide to handle this message, process it outside of the netty event loop
|
||||
if (shouldHandleRequest(ctx, request))
|
||||
dispatcher.dispatch(ctx.channel(), request, this::toFlushItem);
|
||||
}
|
||||
|
||||
// Acts as a Dispatcher.FlushItemConverter
|
||||
private Flusher.FlushItem.Unframed toFlushItem(Channel channel, Message.Request request, Message.Response response)
|
||||
{
|
||||
return new Flusher.FlushItem.Unframed(channel, response, request.getSource(), this::releaseItem);
|
||||
}
|
||||
|
||||
private void releaseItem(Flusher.FlushItem<Message.Response> item)
|
||||
{
|
||||
// Note: in contrast to the equivalent for V5 protocol, CQLMessageHandler::release(FlushItem item),
|
||||
// this does not release the FlushItem's Message.Response. In V4, the buffers for the response's body
|
||||
// and serialised header are emitted directly down the Netty pipeline from Envelope.Encoder, so
|
||||
// releasing them is handled by the pipeline itself.
|
||||
long itemSize = item.request.header.bodySizeInBytes;
|
||||
item.request.release();
|
||||
|
||||
// since the request has been processed, decrement inflight payload at channel, endpoint and global levels
|
||||
channelPayloadBytesInFlight -= itemSize;
|
||||
ResourceLimits.Outcome endpointGlobalReleaseOutcome = endpointPayloadTracker.release(itemSize);
|
||||
|
||||
// now check to see if we need to reenable the channel's autoRead.
|
||||
// If the current payload side is zero, we must reenable autoread as
|
||||
// 1) we allow no other thread/channel to do it, and
|
||||
// 2) there's no other events following this one (becuase we're at zero bytes in flight),
|
||||
// so no successive to trigger the other clause in this if-block
|
||||
//
|
||||
// note: this path is only relevant when part of a pre-V5 pipeline, as only in this case is
|
||||
// paused ever set to true. In pipelines configured for V5 or later, backpressure and control
|
||||
// over the inbound pipeline's autoread status are handled by the FrameDecoder/FrameProcessor.
|
||||
ChannelConfig config = item.channel.config();
|
||||
if (paused && (channelPayloadBytesInFlight == 0 || endpointGlobalReleaseOutcome == ResourceLimits.Outcome.BELOW_LIMIT))
|
||||
{
|
||||
paused = false;
|
||||
ClientMetrics.instance.unpauseConnection();
|
||||
config.setAutoRead(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This check for inflight payload to potentially discard the request should have been ideally in one of the
|
||||
* first handlers in the pipeline (Envelope.Decoder::decode()). However, incase of any exception thrown between that
|
||||
* handler (where inflight payload is incremented) and this handler (Dispatcher::channelRead0) (where inflight
|
||||
* payload in decremented), inflight payload becomes erroneous. ExceptionHandler is not sufficient for this
|
||||
* purpose since it does not have the message envelope associated with the exception.
|
||||
* <p>
|
||||
* Note: this method should execute on the netty event loop.
|
||||
*/
|
||||
private boolean shouldHandleRequest(ChannelHandlerContext ctx, Message.Request request)
|
||||
{
|
||||
long requestSize = request.getSource().header.bodySizeInBytes;
|
||||
|
||||
// check for overloaded state by trying to allocate the message size from inflight payload trackers
|
||||
if (endpointPayloadTracker.tryAllocate(requestSize) != ResourceLimits.Outcome.SUCCESS)
|
||||
{
|
||||
if (request.connection.isThrowOnOverload())
|
||||
{
|
||||
// discard the request and throw an exception
|
||||
ClientMetrics.instance.markRequestDiscarded();
|
||||
logger.trace("Discarded request of size: {}. InflightChannelRequestPayload: {}, {}, Request: {}",
|
||||
requestSize,
|
||||
channelPayloadBytesInFlight,
|
||||
endpointPayloadTracker.toString(),
|
||||
request);
|
||||
throw ErrorMessage.wrap(new OverloadedException("Server is in overloaded state. Cannot accept more requests at this point"),
|
||||
request.getSource().header.streamId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set backpressure on the channel, and handle the request
|
||||
endpointPayloadTracker.allocate(requestSize);
|
||||
ctx.channel().config().setAutoRead(false);
|
||||
ClientMetrics.instance.pauseConnection();
|
||||
paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
channelPayloadBytesInFlight += requestSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx)
|
||||
{
|
||||
endpointPayloadTracker.release();
|
||||
if (paused)
|
||||
{
|
||||
paused = false;
|
||||
ClientMetrics.instance.unpauseConnection();
|
||||
}
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple adaptor to allow {@link org.apache.cassandra.transport.Message.Decoder#decodeMessage(Channel, Envelope)}
|
||||
* to be used as a handler in pre-V5 pipelines
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
public static class ProtocolDecoder extends MessageToMessageDecoder<Envelope>
|
||||
{
|
||||
public static final ProtocolDecoder instance = new ProtocolDecoder();
|
||||
private ProtocolDecoder(){}
|
||||
|
||||
public void decode(ChannelHandlerContext ctx, Envelope source, List results)
|
||||
{
|
||||
try
|
||||
{
|
||||
results.add(Message.Decoder.decodeMessage(ctx.channel(), source));
|
||||
}
|
||||
catch (Throwable ex)
|
||||
{
|
||||
source.release();
|
||||
// Remember the streamId
|
||||
throw ErrorMessage.wrap(ex, source.header.streamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple adaptor to plug CQL message encoding into pre-V5 pipelines
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
public static class ProtocolEncoder extends MessageToMessageEncoder<Message>
|
||||
{
|
||||
public static final ProtocolEncoder instance = new ProtocolEncoder();
|
||||
private ProtocolEncoder(){}
|
||||
|
||||
public void encode(ChannelHandlerContext ctx, Message source, List results)
|
||||
{
|
||||
Connection connection = ctx.channel().attr(Connection.attributeKey).get();
|
||||
// The only case the connection can be null is when we send the initial STARTUP message (client side thus)
|
||||
ProtocolVersion version = connection == null ? ProtocolVersion.CURRENT : connection.getVersion();
|
||||
results.add(source.encode(version));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-V5 exception handler which closes the connection if an {@link org.apache.cassandra.transport.ProtocolException}
|
||||
* is thrown
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
public static final class ExceptionHandler extends ChannelInboundHandlerAdapter
|
||||
{
|
||||
public static final ExceptionHandler instance = new ExceptionHandler();
|
||||
private ExceptionHandler(){}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final ChannelHandlerContext ctx, Throwable cause)
|
||||
{
|
||||
// Provide error message to client in case channel is still open
|
||||
ExceptionHandlers.UnexpectedChannelExceptionHandler handler = new ExceptionHandlers.UnexpectedChannelExceptionHandler(ctx.channel(), false);
|
||||
ErrorMessage errorMessage = ErrorMessage.fromException(cause, handler);
|
||||
if (ctx.channel().isOpen())
|
||||
{
|
||||
ChannelFuture future = ctx.writeAndFlush(errorMessage.encode(ProtocolVersion.CURRENT));
|
||||
// On protocol exception, close the channel as soon as the message have been sent
|
||||
if (cause instanceof ProtocolException)
|
||||
future.addListener((ChannelFutureListener) f -> ctx.close());
|
||||
}
|
||||
JVMStabilityInspector.inspectThrowable(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ import org.apache.commons.lang3.ArrayUtils;
|
|||
* The native (CQL binary) protocol version.
|
||||
*
|
||||
* Some versions may be in beta, which means that the client must
|
||||
* specify the beta flag in the frame for the version to be considered valid.
|
||||
* specify the beta flag in the envelope's header for the version to be considered valid.
|
||||
* Beta versions must have the word "beta" in their description, this is mandated
|
||||
* by the specs.
|
||||
*
|
||||
|
|
@ -67,7 +67,7 @@ public enum ProtocolVersion implements Comparable<ProtocolVersion>
|
|||
final static ProtocolVersion MAX_SUPPORTED_VERSION = SUPPORTED_VERSIONS[SUPPORTED_VERSIONS.length - 1];
|
||||
|
||||
/** All supported versions, published as an enumset */
|
||||
public final static EnumSet<ProtocolVersion> SUPPORTED = EnumSet.copyOf(Arrays.asList((ProtocolVersion[]) ArrayUtils.addAll(SUPPORTED_VERSIONS)));
|
||||
public final static EnumSet<ProtocolVersion> SUPPORTED = EnumSet.copyOf(Arrays.asList(ArrayUtils.addAll(SUPPORTED_VERSIONS)));
|
||||
|
||||
/** Old unsupported versions, this is OK as long as we never add newer unsupported versions */
|
||||
public final static EnumSet<ProtocolVersion> UNSUPPORTED = EnumSet.complementOf(SUPPORTED);
|
||||
|
|
@ -136,11 +136,6 @@ public enum ProtocolVersion implements Comparable<ProtocolVersion>
|
|||
return num;
|
||||
}
|
||||
|
||||
public boolean supportsChecksums()
|
||||
{
|
||||
return num >= V5.asInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,38 +17,23 @@
|
|||
*/
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.codahale.metrics.Reservoir;
|
||||
import com.codahale.metrics.Snapshot;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.epoll.EpollEventLoopGroup;
|
||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||
import io.netty.channel.group.ChannelGroup;
|
||||
import io.netty.channel.group.DefaultChannelGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import io.netty.util.concurrent.EventExecutor;
|
||||
import io.netty.util.concurrent.GlobalEventExecutor;
|
||||
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||
import io.netty.util.internal.logging.Slf4JLoggerFactory;
|
||||
|
|
@ -57,11 +42,8 @@ import org.apache.cassandra.config.DatabaseDescriptor;
|
|||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.metrics.DecayingEstimatedHistogramReservoir;
|
||||
import org.apache.cassandra.net.ResourceLimits;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.SchemaChangeListener;
|
||||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.service.*;
|
||||
import org.apache.cassandra.transport.messages.EventMessage;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
|
@ -89,8 +71,8 @@ public class Server implements CassandraDaemon.Server
|
|||
public final InetSocketAddress socket;
|
||||
public final EncryptionOptions.TlsEncryptionPolicy tlsEncryptionPolicy;
|
||||
private final AtomicBoolean isRunning = new AtomicBoolean(false);
|
||||
|
||||
private EventLoopGroup workerGroup;
|
||||
private final PipelineConfigurator pipelineConfigurator;
|
||||
private final EventLoopGroup workerGroup;
|
||||
|
||||
private Server (Builder builder)
|
||||
{
|
||||
|
|
@ -107,6 +89,14 @@ public class Server implements CassandraDaemon.Server
|
|||
else
|
||||
workerGroup = new NioEventLoopGroup();
|
||||
}
|
||||
|
||||
pipelineConfigurator = builder.pipelineConfigurator != null
|
||||
? builder.pipelineConfigurator
|
||||
: new PipelineConfigurator(useEpoll,
|
||||
DatabaseDescriptor.getRpcKeepAlive(),
|
||||
DatabaseDescriptor.useNativeTransportLegacyFlusher(),
|
||||
builder.tlsEncryptionPolicy);
|
||||
|
||||
EventNotifier notifier = new EventNotifier(this);
|
||||
StorageService.instance.register(notifier);
|
||||
Schema.instance.registerListener(notifier);
|
||||
|
|
@ -129,40 +119,7 @@ public class Server implements CassandraDaemon.Server
|
|||
return;
|
||||
|
||||
// Configure the server.
|
||||
ServerBootstrap bootstrap = new ServerBootstrap()
|
||||
.channel(useEpoll ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.childOption(ChannelOption.SO_LINGER, 0)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, DatabaseDescriptor.getRpcKeepAlive())
|
||||
.childOption(ChannelOption.ALLOCATOR, CBUtil.allocator)
|
||||
.childOption(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 32 * 1024)
|
||||
.childOption(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024);
|
||||
if (workerGroup != null)
|
||||
bootstrap = bootstrap.group(workerGroup);
|
||||
|
||||
final EncryptionOptions clientEnc = DatabaseDescriptor.getNativeProtocolEncryptionOptions();
|
||||
|
||||
switch (this.tlsEncryptionPolicy)
|
||||
{
|
||||
case UNENCRYPTED:
|
||||
bootstrap.childHandler(new Initializer(this));
|
||||
break;
|
||||
case OPTIONAL:
|
||||
logger.debug("Enabling optionally encrypted CQL connections between client and server");
|
||||
bootstrap.childHandler(new OptionalSecureInitializer(this, clientEnc));
|
||||
break;
|
||||
case ENCRYPTED:
|
||||
logger.debug("Enabling encrypted CQL connections between client and server");
|
||||
bootstrap.childHandler(new SecureInitializer(this, clientEnc));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized TLS encryption policy: " + this.tlsEncryptionPolicy);
|
||||
}
|
||||
|
||||
// Bind and start to accept incoming connections.
|
||||
logger.info("Starting listening for CQL clients on {} ({})...", socket, clientEnc.tlsEncryptionPolicy().description());
|
||||
|
||||
ChannelFuture bindFuture = bootstrap.bind(socket);
|
||||
ChannelFuture bindFuture = pipelineConfigurator.initializeChannel(workerGroup, socket, connectionFactory);
|
||||
if (!bindFuture.awaitUninterruptibly().isSuccess())
|
||||
throw new IllegalStateException(String.format("Failed to bind port %d on %s.", socket.getPort(), socket.getAddress().getHostAddress()),
|
||||
bindFuture.cause());
|
||||
|
|
@ -215,11 +172,11 @@ public class Server implements CassandraDaemon.Server
|
|||
public static class Builder
|
||||
{
|
||||
private EventLoopGroup workerGroup;
|
||||
private EventExecutor eventExecutorGroup;
|
||||
private EncryptionOptions.TlsEncryptionPolicy tlsEncryptionPolicy = EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
|
||||
private InetAddress hostAddr;
|
||||
private int port = -1;
|
||||
private InetSocketAddress socket;
|
||||
private PipelineConfigurator pipelineConfigurator;
|
||||
|
||||
public Builder withTlsEncryptionPolicy(EncryptionOptions.TlsEncryptionPolicy tlsEncryptionPolicy)
|
||||
{
|
||||
|
|
@ -247,6 +204,12 @@ public class Server implements CassandraDaemon.Server
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder withPipelineConfigurator(PipelineConfigurator configurator)
|
||||
{
|
||||
this.pipelineConfigurator = configurator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Server build()
|
||||
{
|
||||
return new Server(this);
|
||||
|
|
@ -334,256 +297,6 @@ public class Server implements CassandraDaemon.Server
|
|||
|
||||
}
|
||||
|
||||
// global inflight payload across all channels across all endpoints
|
||||
private static final ResourceLimits.Concurrent globalRequestPayloadInFlight = new ResourceLimits.Concurrent(DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytes());
|
||||
|
||||
public static class EndpointPayloadTracker
|
||||
{
|
||||
// inflight payload per endpoint across corresponding channels
|
||||
private static final ConcurrentMap<InetAddress, EndpointPayloadTracker> requestPayloadInFlightPerEndpoint = new ConcurrentHashMap<>();
|
||||
|
||||
private final AtomicInteger refCount = new AtomicInteger(0);
|
||||
private final InetAddress endpoint;
|
||||
|
||||
final ResourceLimits.EndpointAndGlobal endpointAndGlobalPayloadsInFlight = new ResourceLimits.EndpointAndGlobal(new ResourceLimits.Concurrent(DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp()),
|
||||
globalRequestPayloadInFlight);
|
||||
|
||||
private EndpointPayloadTracker(InetAddress endpoint)
|
||||
{
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public static EndpointPayloadTracker get(InetAddress endpoint)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
EndpointPayloadTracker result = requestPayloadInFlightPerEndpoint.computeIfAbsent(endpoint, EndpointPayloadTracker::new);
|
||||
if (result.acquire())
|
||||
return result;
|
||||
|
||||
requestPayloadInFlightPerEndpoint.remove(endpoint, result);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getCurrentGlobalUsage()
|
||||
{
|
||||
return globalRequestPayloadInFlight.using();
|
||||
}
|
||||
|
||||
public static Snapshot getCurrentIpUsage()
|
||||
{
|
||||
DecayingEstimatedHistogramReservoir histogram = new DecayingEstimatedHistogramReservoir();
|
||||
for (EndpointPayloadTracker tracker : requestPayloadInFlightPerEndpoint.values())
|
||||
{
|
||||
histogram.update(tracker.endpointAndGlobalPayloadsInFlight.endpoint().using());
|
||||
}
|
||||
return histogram.getSnapshot();
|
||||
}
|
||||
|
||||
public static long getGlobalLimit()
|
||||
{
|
||||
return DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytes();
|
||||
}
|
||||
|
||||
public static void setGlobalLimit(long newLimit)
|
||||
{
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytes(newLimit);
|
||||
long existingLimit = globalRequestPayloadInFlight.setLimit(DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytes());
|
||||
|
||||
logger.info("Changed native_max_transport_requests_in_bytes from {} to {}", existingLimit, newLimit);
|
||||
}
|
||||
|
||||
public static long getEndpointLimit()
|
||||
{
|
||||
return DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp();
|
||||
}
|
||||
|
||||
public static void setEndpointLimit(long newLimit)
|
||||
{
|
||||
long existingLimit = DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp();
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytesPerIp(newLimit); // ensure new trackers get the new limit
|
||||
for (EndpointPayloadTracker tracker : requestPayloadInFlightPerEndpoint.values())
|
||||
existingLimit = tracker.endpointAndGlobalPayloadsInFlight.endpoint().setLimit(newLimit);
|
||||
|
||||
logger.info("Changed native_max_transport_requests_in_bytes_per_ip from {} to {}", existingLimit, newLimit);
|
||||
}
|
||||
|
||||
private boolean acquire()
|
||||
{
|
||||
return 0 < refCount.updateAndGet(i -> i < 0 ? i : i + 1);
|
||||
}
|
||||
|
||||
public void release()
|
||||
{
|
||||
if (-1 == refCount.updateAndGet(i -> i == 1 ? -1 : i - 1))
|
||||
requestPayloadInFlightPerEndpoint.remove(endpoint, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will recompute the ip usage histo on each query of the snapshot when requested instead of trying to keep
|
||||
* a histogram up to date with each request
|
||||
*/
|
||||
public static Reservoir ipUsageReservoir()
|
||||
{
|
||||
return new Reservoir()
|
||||
{
|
||||
public int size()
|
||||
{
|
||||
return requestPayloadInFlightPerEndpoint.size();
|
||||
}
|
||||
|
||||
public void update(long l)
|
||||
{
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
public Snapshot getSnapshot()
|
||||
{
|
||||
return getCurrentIpUsage();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class Initializer extends ChannelInitializer<Channel>
|
||||
{
|
||||
// Stateless handlers
|
||||
private static final Message.ProtocolDecoder messageDecoder = new Message.ProtocolDecoder();
|
||||
private static final Message.ProtocolEncoder messageEncoder = new Message.ProtocolEncoder();
|
||||
private static final Frame.InboundBodyTransformer inboundFrameTransformer = new Frame.InboundBodyTransformer();
|
||||
private static final Frame.OutboundBodyTransformer outboundFrameTransformer = new Frame.OutboundBodyTransformer();
|
||||
private static final Frame.Encoder frameEncoder = new Frame.Encoder();
|
||||
private static final Message.ExceptionHandler exceptionHandler = new Message.ExceptionHandler();
|
||||
private static final ConnectionLimitHandler connectionLimitHandler = new ConnectionLimitHandler();
|
||||
|
||||
private final Server server;
|
||||
|
||||
public Initializer(Server server)
|
||||
{
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
protected void initChannel(Channel channel) throws Exception
|
||||
{
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
|
||||
// Add the ConnectionLimitHandler to the pipeline if configured to do so.
|
||||
if (DatabaseDescriptor.getNativeTransportMaxConcurrentConnections() > 0
|
||||
|| DatabaseDescriptor.getNativeTransportMaxConcurrentConnectionsPerIp() > 0)
|
||||
{
|
||||
// Add as first to the pipeline so the limit is enforced as first action.
|
||||
pipeline.addFirst("connectionLimitHandler", connectionLimitHandler);
|
||||
}
|
||||
|
||||
long idleTimeout = DatabaseDescriptor.nativeTransportIdleTimeout();
|
||||
if (idleTimeout > 0)
|
||||
{
|
||||
pipeline.addLast("idleStateHandler", new IdleStateHandler(false, 0, 0, idleTimeout, TimeUnit.MILLISECONDS)
|
||||
{
|
||||
@Override
|
||||
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt)
|
||||
{
|
||||
logger.info("Closing client connection {} after timeout of {}ms", channel.remoteAddress(), idleTimeout);
|
||||
ctx.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//pipeline.addLast("debug", new LoggingHandler());
|
||||
|
||||
pipeline.addLast("frameDecoder", new Frame.Decoder(server.connectionFactory));
|
||||
pipeline.addLast("frameEncoder", frameEncoder);
|
||||
|
||||
pipeline.addLast("inboundFrameTransformer", inboundFrameTransformer);
|
||||
pipeline.addLast("outboundFrameTransformer", outboundFrameTransformer);
|
||||
|
||||
pipeline.addLast("messageDecoder", messageDecoder);
|
||||
pipeline.addLast("messageEncoder", messageEncoder);
|
||||
|
||||
pipeline.addLast("executor", new Message.Dispatcher(DatabaseDescriptor.useNativeTransportLegacyFlusher(),
|
||||
EndpointPayloadTracker.get(((InetSocketAddress) channel.remoteAddress()).getAddress())));
|
||||
|
||||
// The exceptionHandler will take care of handling exceptionCaught(...) events while still running
|
||||
// on the same EventLoop as all previous added handlers in the pipeline. This is important as the used
|
||||
// eventExecutorGroup may not enforce strict ordering for channel events.
|
||||
// As the exceptionHandler runs in the EventLoop as the previous handlers we are sure all exceptions are
|
||||
// correctly handled before the handler itself is removed.
|
||||
// See https://issues.apache.org/jira/browse/CASSANDRA-13649
|
||||
pipeline.addLast("exceptionHandler", exceptionHandler);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract static class AbstractSecureIntializer extends Initializer
|
||||
{
|
||||
private final EncryptionOptions encryptionOptions;
|
||||
|
||||
protected AbstractSecureIntializer(Server server, EncryptionOptions encryptionOptions)
|
||||
{
|
||||
super(server);
|
||||
this.encryptionOptions = encryptionOptions;
|
||||
}
|
||||
|
||||
protected final SslHandler createSslHandler(ByteBufAllocator allocator) throws IOException
|
||||
{
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, encryptionOptions.require_client_auth, SSLFactory.SocketType.SERVER);
|
||||
return sslContext.newHandler(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OptionalSecureInitializer extends AbstractSecureIntializer
|
||||
{
|
||||
public OptionalSecureInitializer(Server server, EncryptionOptions encryptionOptions)
|
||||
{
|
||||
super(server, encryptionOptions);
|
||||
}
|
||||
|
||||
protected void initChannel(final Channel channel) throws Exception
|
||||
{
|
||||
super.initChannel(channel);
|
||||
channel.pipeline().addFirst("sslDetectionHandler", new ByteToMessageDecoder()
|
||||
{
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception
|
||||
{
|
||||
if (byteBuf.readableBytes() < 5)
|
||||
{
|
||||
// To detect if SSL must be used we need to have at least 5 bytes, so return here and try again
|
||||
// once more bytes a ready.
|
||||
return;
|
||||
}
|
||||
if (SslHandler.isEncrypted(byteBuf))
|
||||
{
|
||||
// Connection uses SSL/TLS, replace the detection handler with a SslHandler and so use
|
||||
// encryption.
|
||||
SslHandler sslHandler = createSslHandler(channel.alloc());
|
||||
channelHandlerContext.pipeline().replace(this, "ssl", sslHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Connection use no TLS/SSL encryption, just remove the detection handler and continue without
|
||||
// SslHandler in the pipeline.
|
||||
channelHandlerContext.pipeline().remove(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class SecureInitializer extends AbstractSecureIntializer
|
||||
{
|
||||
public SecureInitializer(Server server, EncryptionOptions encryptionOptions)
|
||||
{
|
||||
super(server, encryptionOptions);
|
||||
}
|
||||
|
||||
protected void initChannel(Channel channel) throws Exception
|
||||
{
|
||||
SslHandler sslHandler = createSslHandler(channel.alloc());
|
||||
super.initChannel(channel);
|
||||
channel.pipeline().addFirst("ssl", sslHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private static class LatestEvent
|
||||
{
|
||||
public final Event.StatusChange.Status status;
|
||||
|
|
|
|||
|
|
@ -21,46 +21,34 @@ import java.io.Closeable;
|
|||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import io.netty.util.concurrent.PromiseCombiner;
|
||||
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||
import io.netty.util.internal.logging.Slf4JLoggerFactory;
|
||||
import org.apache.cassandra.concurrent.NamedThreadFactory;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.db.ConsistencyLevel;
|
||||
import org.apache.cassandra.net.*;
|
||||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.transport.frame.checksum.ChecksummingTransformer;
|
||||
import org.apache.cassandra.transport.frame.compress.CompressingTransformer;
|
||||
import org.apache.cassandra.transport.frame.compress.Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.LZ4Compressor;
|
||||
import org.apache.cassandra.transport.messages.ErrorMessage;
|
||||
import org.apache.cassandra.transport.messages.EventMessage;
|
||||
import org.apache.cassandra.transport.messages.ExecuteMessage;
|
||||
import org.apache.cassandra.transport.messages.PrepareMessage;
|
||||
import org.apache.cassandra.transport.messages.QueryMessage;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
import org.apache.cassandra.transport.messages.StartupMessage;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import org.apache.cassandra.utils.ChecksumType;
|
||||
import org.apache.cassandra.transport.messages.*;
|
||||
|
||||
import static org.apache.cassandra.transport.CQLMessageHandler.envelopeSize;
|
||||
import static org.apache.cassandra.transport.Flusher.MAX_FRAMED_PAYLOAD_SIZE;
|
||||
|
||||
public class SimpleClient implements Closeable
|
||||
{
|
||||
|
|
@ -70,9 +58,11 @@ public class SimpleClient implements Closeable
|
|||
}
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SimpleClient.class);
|
||||
|
||||
public final String host;
|
||||
public final int port;
|
||||
private final EncryptionOptions encryptionOptions;
|
||||
private final int largeMessageThreshold;
|
||||
|
||||
protected final ResponseHandler responseHandler = new ResponseHandler();
|
||||
protected final Connection.Tracker tracker = new ConnectionTracker();
|
||||
|
|
@ -83,13 +73,68 @@ public class SimpleClient implements Closeable
|
|||
protected Channel channel;
|
||||
protected ChannelFuture lastWriteFuture;
|
||||
|
||||
private final Connection.Factory connectionFactory = new Connection.Factory()
|
||||
protected String compression;
|
||||
|
||||
public static class Builder
|
||||
{
|
||||
public Connection newConnection(Channel channel, ProtocolVersion version)
|
||||
private final String host;
|
||||
private final int port;
|
||||
private EncryptionOptions encryptionOptions = new EncryptionOptions();
|
||||
private ProtocolVersion version = ProtocolVersion.CURRENT;
|
||||
private boolean useBeta = false;
|
||||
private int largeMessageThreshold = FrameEncoder.Payload.MAX_SIZE;
|
||||
|
||||
private Builder(String host, int port)
|
||||
{
|
||||
return connection;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
};
|
||||
|
||||
public Builder encryption(EncryptionOptions options)
|
||||
{
|
||||
this.encryptionOptions = options;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder useBeta()
|
||||
{
|
||||
this.useBeta = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder protocolVersion(ProtocolVersion version)
|
||||
{
|
||||
this.version = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder largeMessageThreshold(int bytes)
|
||||
{
|
||||
largeMessageThreshold = bytes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SimpleClient build()
|
||||
{
|
||||
if (version.isBeta() && !useBeta)
|
||||
throw new IllegalArgumentException(String.format("Beta version of server used (%s), but USE_BETA flag is not set", version));
|
||||
return new SimpleClient(this);
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder builder(String host, int port)
|
||||
{
|
||||
return new Builder(host, port);
|
||||
}
|
||||
|
||||
private SimpleClient(Builder builder)
|
||||
{
|
||||
this.host = builder.host;
|
||||
this.port = builder.port;
|
||||
this.version = builder.version;
|
||||
this.encryptionOptions = builder.encryptionOptions.applyConfig();
|
||||
this.largeMessageThreshold = builder.largeMessageThreshold;
|
||||
}
|
||||
|
||||
public SimpleClient(String host, int port, ProtocolVersion version, EncryptionOptions encryptionOptions)
|
||||
{
|
||||
|
|
@ -115,6 +160,9 @@ public class SimpleClient implements Closeable
|
|||
|
||||
this.version = version;
|
||||
this.encryptionOptions = new EncryptionOptions(encryptionOptions).applyConfig();
|
||||
this.largeMessageThreshold = FrameEncoder.Payload.MAX_SIZE -
|
||||
Math.max(FrameEncoderCrc.HEADER_AND_TRAILER_LENGTH,
|
||||
FrameEncoderLZ4.HEADER_AND_TRAILER_LENGTH);
|
||||
}
|
||||
|
||||
public SimpleClient(String host, int port)
|
||||
|
|
@ -122,12 +170,12 @@ public class SimpleClient implements Closeable
|
|||
this(host, port, new EncryptionOptions());
|
||||
}
|
||||
|
||||
public SimpleClient connect(boolean useCompression, boolean useChecksums) throws IOException
|
||||
public SimpleClient connect(boolean useCompression) throws IOException
|
||||
{
|
||||
return connect(useCompression, useChecksums, false);
|
||||
return connect(useCompression, false);
|
||||
}
|
||||
|
||||
public SimpleClient connect(boolean useCompression, boolean useChecksums, boolean throwOnOverload) throws IOException
|
||||
public SimpleClient connect(boolean useCompression, boolean throwOnOverload) throws IOException
|
||||
{
|
||||
establishConnection();
|
||||
|
||||
|
|
@ -137,20 +185,13 @@ public class SimpleClient implements Closeable
|
|||
options.put(StartupMessage.THROW_ON_OVERLOAD, "1");
|
||||
connection.setThrowOnOverload(throwOnOverload);
|
||||
|
||||
if (useChecksums)
|
||||
if (useCompression)
|
||||
{
|
||||
Compressor compressor = useCompression ? LZ4Compressor.INSTANCE : null;
|
||||
connection.setTransformer(ChecksummingTransformer.getTransformer(ChecksumType.CRC32, compressor));
|
||||
options.put(StartupMessage.CHECKSUM, "crc32");
|
||||
options.put(StartupMessage.COMPRESSION, "lz4");
|
||||
options.put(StartupMessage.COMPRESSION, "LZ4");
|
||||
connection.setCompressor(Compressor.LZ4Compressor.instance);
|
||||
}
|
||||
else if (useCompression)
|
||||
{
|
||||
connection.setTransformer(CompressingTransformer.getTransformer(LZ4Compressor.INSTANCE));
|
||||
options.put(StartupMessage.COMPRESSION, "lz4");
|
||||
}
|
||||
|
||||
execute(new StartupMessage(options));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -163,18 +204,18 @@ public class SimpleClient implements Closeable
|
|||
{
|
||||
// Configure the client.
|
||||
bootstrap = new Bootstrap()
|
||||
.group(new NioEventLoopGroup())
|
||||
.group(new NioEventLoopGroup(new NamedThreadFactory("SimpleClient-nioEventLoopGroup")))
|
||||
.channel(io.netty.channel.socket.nio.NioSocketChannel.class)
|
||||
.option(ChannelOption.TCP_NODELAY, true);
|
||||
|
||||
// Configure the pipeline factory.
|
||||
if(encryptionOptions.isEnabled())
|
||||
{
|
||||
bootstrap.handler(new SecureInitializer());
|
||||
bootstrap.handler(new SecureInitializer(largeMessageThreshold));
|
||||
}
|
||||
else
|
||||
{
|
||||
bootstrap.handler(new Initializer());
|
||||
bootstrap.handler(new Initializer(largeMessageThreshold));
|
||||
}
|
||||
ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port));
|
||||
|
||||
|
|
@ -232,8 +273,10 @@ public class SimpleClient implements Closeable
|
|||
try
|
||||
{
|
||||
request.attach(connection);
|
||||
lastWriteFuture = channel.writeAndFlush(request);
|
||||
Message.Response msg = responseHandler.responses.take();
|
||||
lastWriteFuture = channel.writeAndFlush(Collections.singletonList(request));
|
||||
Message.Response msg = responseHandler.responses.poll(10, TimeUnit.SECONDS);
|
||||
if (msg == null)
|
||||
throw new RuntimeException("timeout");
|
||||
if (msg instanceof ErrorMessage)
|
||||
throw new RuntimeException((Throwable)((ErrorMessage)msg).error);
|
||||
return msg;
|
||||
|
|
@ -244,6 +287,48 @@ public class SimpleClient implements Closeable
|
|||
}
|
||||
}
|
||||
|
||||
public Map<Message.Request, Message.Response> execute(List<Message.Request> requests)
|
||||
{
|
||||
try
|
||||
{
|
||||
Map<Message.Request, Message.Response> rrMap = new HashMap<>();
|
||||
|
||||
if (version.isGreaterOrEqualTo(ProtocolVersion.V5))
|
||||
{
|
||||
for (int i = 0; i < requests.size(); i++)
|
||||
{
|
||||
Message.Request message = requests.get(i);
|
||||
message.setStreamId(i);
|
||||
message.attach(connection);
|
||||
}
|
||||
lastWriteFuture = channel.writeAndFlush(requests);
|
||||
|
||||
long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10);
|
||||
for (int i = 0; i < requests.size(); i++)
|
||||
{
|
||||
Message.Response msg = responseHandler.responses.poll(deadline - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
|
||||
if (msg == null)
|
||||
throw new RuntimeException("timeout");
|
||||
if (msg instanceof ErrorMessage)
|
||||
throw new RuntimeException((Throwable) ((ErrorMessage) msg).error);
|
||||
rrMap.put(requests.get(msg.getStreamId()), msg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// V4 doesn't support batching
|
||||
for (Message.Request request : requests)
|
||||
rrMap.put(request, execute(request));
|
||||
}
|
||||
|
||||
return rrMap;
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public interface EventHandler
|
||||
{
|
||||
void onEvent(Event event);
|
||||
|
|
@ -259,46 +344,250 @@ public class SimpleClient implements Closeable
|
|||
}
|
||||
}
|
||||
|
||||
// Stateless handlers
|
||||
private static final Message.ProtocolDecoder messageDecoder = new Message.ProtocolDecoder();
|
||||
private static final Message.ProtocolEncoder messageEncoder = new Message.ProtocolEncoder();
|
||||
private static final Frame.InboundBodyTransformer inboundFrameTransformer = new Frame.InboundBodyTransformer();
|
||||
private static final Frame.OutboundBodyTransformer outboundFrameTransformer = new Frame.OutboundBodyTransformer();
|
||||
private static final Frame.Encoder frameEncoder = new Frame.Encoder();
|
||||
|
||||
private static class ConnectionTracker implements Connection.Tracker
|
||||
{
|
||||
public void addConnection(Channel ch, Connection connection) {}
|
||||
}
|
||||
|
||||
public boolean isRegistered(Event.Type type, Channel ch)
|
||||
private static class HandlerNames
|
||||
{
|
||||
private static final String ENVELOPE_DECODER = "envelopeDecoder";
|
||||
private static final String ENVELOPE_ENCODER = "envelopeEncoder";
|
||||
private static final String COMPRESSOR = "compressor";
|
||||
private static final String DECOMPRESSOR = "decompressor";
|
||||
private static final String MESSAGE_DECODER = "messageDecoder";
|
||||
private static final String MESSAGE_ENCODER = "messageEncoder";
|
||||
|
||||
private static final String INITIAL_HANDLER = "intitialHandler";
|
||||
private static final String RESPONSE_HANDLER = "responseHandler";
|
||||
|
||||
private static final String FRAME_DECODER = "frameDecoder";
|
||||
private static final String FRAME_ENCODER = "frameEncoder";
|
||||
private static final String PROCESSOR = "processor";
|
||||
}
|
||||
|
||||
private static class InitialHandler extends MessageToMessageDecoder<Envelope>
|
||||
{
|
||||
final ProtocolVersion version;
|
||||
final ResponseHandler responseHandler;
|
||||
final int largeMessageThreshold;
|
||||
InitialHandler(ProtocolVersion version, ResponseHandler responseHandler, int largeMessageThreshold)
|
||||
{
|
||||
return false;
|
||||
this.version = version;
|
||||
this.responseHandler = responseHandler;
|
||||
this.largeMessageThreshold = largeMessageThreshold;
|
||||
}
|
||||
|
||||
protected void decode(ChannelHandlerContext ctx, Envelope response, List<Object> results)
|
||||
{
|
||||
switch(response.header.type)
|
||||
{
|
||||
case READY:
|
||||
case AUTHENTICATE:
|
||||
if (response.header.version.isGreaterOrEqualTo(ProtocolVersion.V5))
|
||||
{
|
||||
configureModernPipeline(ctx, response);
|
||||
// consuming the message is done when setting up the pipeline
|
||||
}
|
||||
else
|
||||
{
|
||||
configureLegacyPipeline(ctx);
|
||||
// really just removes self from the pipeline, so pass this message on
|
||||
ctx.pipeline().context(Envelope.Decoder.class).fireChannelRead(response);
|
||||
}
|
||||
break;
|
||||
case SUPPORTED:
|
||||
// just pass through
|
||||
results.add(response);
|
||||
break;
|
||||
default:
|
||||
throw new ProtocolException(String.format("Unexpected %s response expecting " +
|
||||
"READY, AUTHENTICATE or SUPPORTED",
|
||||
response.header.type));
|
||||
}
|
||||
}
|
||||
|
||||
private void configureModernPipeline(ChannelHandlerContext ctx, Envelope response)
|
||||
{
|
||||
logger.info("Configuring modern pipeline");
|
||||
ChannelPipeline pipeline = ctx.pipeline();
|
||||
pipeline.remove(HandlerNames.ENVELOPE_DECODER);
|
||||
pipeline.remove(HandlerNames.MESSAGE_DECODER);
|
||||
pipeline.remove(HandlerNames.MESSAGE_ENCODER);
|
||||
pipeline.remove(HandlerNames.RESPONSE_HANDLER);
|
||||
|
||||
BufferPoolAllocator allocator = GlobalBufferPoolAllocator.instance;
|
||||
Channel channel = ctx.channel();
|
||||
channel.config().setOption(ChannelOption.ALLOCATOR, allocator);
|
||||
int queueCapacity = 1 << 20; // 1MiB
|
||||
|
||||
Envelope.Decoder envelopeDecoder = new Envelope.Decoder();
|
||||
Message.Decoder<Message.Response> messageDecoder = Message.responseDecoder();
|
||||
FrameDecoder frameDecoder = frameDecoder(ctx, allocator);
|
||||
FrameEncoder frameEncoder = frameEncoder(ctx);
|
||||
FrameEncoder.PayloadAllocator payloadAllocator = frameEncoder.allocator();
|
||||
|
||||
CQLMessageHandler.MessageConsumer<Message.Response> responseConsumer = (c, message, converter) -> {
|
||||
responseHandler.handleResponse(c, message);
|
||||
};
|
||||
|
||||
CQLMessageHandler.ErrorHandler errorHandler = (error) -> {
|
||||
throw new RuntimeException("Unexpected error", error);
|
||||
};
|
||||
|
||||
ClientResourceLimits.ResourceProvider resources = new ClientResourceLimits.ResourceProvider()
|
||||
{
|
||||
final ResourceLimits.Limit endpointReserve = new ResourceLimits.Basic(1024 * 1024 * 64);
|
||||
final AbstractMessageHandler.WaitQueue endpointQueue = AbstractMessageHandler.WaitQueue.endpoint(endpointReserve);
|
||||
|
||||
final ResourceLimits.Limit globalReserve = new ResourceLimits.Basic(1024 * 1024 * 64);
|
||||
final AbstractMessageHandler.WaitQueue globalQueue = AbstractMessageHandler.WaitQueue.global(endpointReserve);
|
||||
|
||||
public ResourceLimits.Limit globalLimit()
|
||||
{
|
||||
return globalReserve;
|
||||
}
|
||||
|
||||
public AbstractMessageHandler.WaitQueue globalWaitQueue()
|
||||
{
|
||||
return globalQueue;
|
||||
}
|
||||
|
||||
public ResourceLimits.Limit endpointLimit()
|
||||
{
|
||||
return endpointReserve;
|
||||
}
|
||||
|
||||
public AbstractMessageHandler.WaitQueue endpointWaitQueue()
|
||||
{
|
||||
return endpointQueue;
|
||||
}
|
||||
|
||||
public void release()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
CQLMessageHandler<Message.Response> processor =
|
||||
new CQLMessageHandler<Message.Response>(ctx.channel(),
|
||||
frameDecoder,
|
||||
envelopeDecoder,
|
||||
messageDecoder,
|
||||
responseConsumer,
|
||||
payloadAllocator,
|
||||
queueCapacity,
|
||||
resources,
|
||||
handler -> {},
|
||||
errorHandler,
|
||||
ctx.channel().attr(Connection.attributeKey).get().isThrowOnOverload())
|
||||
{
|
||||
protected void processRequest(Envelope request)
|
||||
{
|
||||
super.processRequest(request);
|
||||
releaseCapacity(Ints.checkedCast(request.header.bodySizeInBytes));
|
||||
}
|
||||
};
|
||||
|
||||
pipeline.addLast(HandlerNames.FRAME_DECODER, frameDecoder);
|
||||
pipeline.addLast(HandlerNames.FRAME_ENCODER, frameEncoder);
|
||||
pipeline.addLast(HandlerNames.PROCESSOR, processor);
|
||||
pipeline.addLast(HandlerNames.MESSAGE_ENCODER, new ChannelOutboundHandlerAdapter() {
|
||||
|
||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception
|
||||
{
|
||||
Connection connection = ctx.channel().attr(Connection.attributeKey).get();
|
||||
// The only case the connection can be null is when we send the initial STARTUP message (client side thus)
|
||||
ProtocolVersion version = connection == null ? ProtocolVersion.CURRENT : connection.getVersion();
|
||||
SimpleFlusher flusher = new SimpleFlusher(frameEncoder);
|
||||
for (Message message : (List<Message>) msg)
|
||||
flusher.enqueue(message.encode(version));
|
||||
|
||||
flusher.maybeWrite(ctx, promise);
|
||||
}
|
||||
});
|
||||
pipeline.remove(this);
|
||||
|
||||
Message.Response message = messageDecoder.decode(ctx.channel(), response);
|
||||
responseConsumer.accept(channel, message, (ch, req, resp) -> null);
|
||||
}
|
||||
|
||||
private FrameDecoder frameDecoder(ChannelHandlerContext ctx, BufferPoolAllocator allocator)
|
||||
{
|
||||
Connection conn = ctx.channel().attr(Connection.attributeKey).get();
|
||||
if (conn.getCompressor() == null)
|
||||
return FrameDecoderCrc.create(allocator);
|
||||
if (conn.getCompressor() instanceof Compressor.LZ4Compressor)
|
||||
return FrameDecoderLZ4.fast(allocator);
|
||||
throw new ProtocolException("Unsupported compressor: " + conn.getCompressor().getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
private FrameEncoder frameEncoder(ChannelHandlerContext ctx)
|
||||
{
|
||||
Connection conn = ctx.channel().attr(Connection.attributeKey).get();
|
||||
if (conn.getCompressor() == null)
|
||||
return FrameEncoderCrc.instance;
|
||||
if (conn.getCompressor() instanceof Compressor.LZ4Compressor)
|
||||
return FrameEncoderLZ4.fastInstance;
|
||||
throw new ProtocolException("Unsupported compressor: " + conn.getCompressor().getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
private void configureLegacyPipeline(ChannelHandlerContext ctx)
|
||||
{
|
||||
logger.info("Configuring legacy pipeline");
|
||||
ChannelPipeline pipeline = ctx.pipeline();
|
||||
pipeline.remove(this);
|
||||
pipeline.addAfter(HandlerNames.ENVELOPE_ENCODER, HandlerNames.DECOMPRESSOR, Envelope.Decompressor.instance);
|
||||
pipeline.addAfter(HandlerNames.DECOMPRESSOR, HandlerNames.COMPRESSOR, Envelope.Compressor.instance);
|
||||
}
|
||||
}
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
static class MessageBatchEncoder extends MessageToMessageEncoder<List<Message>>
|
||||
{
|
||||
public static final MessageBatchEncoder instance = new MessageBatchEncoder();
|
||||
private MessageBatchEncoder(){}
|
||||
|
||||
public void encode(ChannelHandlerContext ctx, List<Message> messages, List<Object> results)
|
||||
{
|
||||
Connection connection = ctx.channel().attr(Connection.attributeKey).get();
|
||||
// The only case the connection can be null is when we send the initial STARTUP message (client side thus)
|
||||
ProtocolVersion version = connection == null ? ProtocolVersion.CURRENT : connection.getVersion();
|
||||
assert messages.size() == 1;
|
||||
results.add(messages.get(0).encode(version));
|
||||
}
|
||||
}
|
||||
|
||||
private class Initializer extends ChannelInitializer<Channel>
|
||||
{
|
||||
private int largeMessageThreshold;
|
||||
Initializer(int largeMessageThreshold)
|
||||
{
|
||||
this.largeMessageThreshold = largeMessageThreshold;
|
||||
}
|
||||
|
||||
protected void initChannel(Channel channel) throws Exception
|
||||
{
|
||||
connection = new Connection(channel, version, tracker);
|
||||
channel.attr(Connection.attributeKey).set(connection);
|
||||
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
pipeline.addLast("frameDecoder", new Frame.Decoder(connectionFactory));
|
||||
pipeline.addLast("frameEncoder", frameEncoder);
|
||||
|
||||
pipeline.addLast("inboundFrameTransformer", inboundFrameTransformer);
|
||||
pipeline.addLast("outboundFrameTransformer", outboundFrameTransformer);
|
||||
|
||||
pipeline.addLast("messageDecoder", messageDecoder);
|
||||
pipeline.addLast("messageEncoder", messageEncoder);
|
||||
|
||||
pipeline.addLast("handler", responseHandler);
|
||||
// pipeline.addLast("debug", new LoggingHandler(LogLevel.INFO));
|
||||
pipeline.addLast(HandlerNames.ENVELOPE_DECODER, new Envelope.Decoder());
|
||||
pipeline.addLast(HandlerNames.ENVELOPE_ENCODER, Envelope.Encoder.instance);
|
||||
pipeline.addLast(HandlerNames.INITIAL_HANDLER, new InitialHandler(version, responseHandler, largeMessageThreshold));
|
||||
pipeline.addLast(HandlerNames.MESSAGE_DECODER, PreV5Handlers.ProtocolDecoder.instance);
|
||||
pipeline.addLast(HandlerNames.MESSAGE_ENCODER, MessageBatchEncoder.instance);
|
||||
pipeline.addLast(HandlerNames.RESPONSE_HANDLER, responseHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private class SecureInitializer extends Initializer
|
||||
{
|
||||
SecureInitializer(int largeMessageThreshold)
|
||||
{
|
||||
super(largeMessageThreshold);
|
||||
}
|
||||
|
||||
protected void initChannel(Channel channel) throws Exception
|
||||
{
|
||||
super.initChannel(channel);
|
||||
|
|
@ -316,9 +605,18 @@ public class SimpleClient implements Closeable
|
|||
|
||||
@Override
|
||||
public void channelRead0(ChannelHandlerContext ctx, Message.Response r)
|
||||
{
|
||||
handleResponse(ctx.channel(), r);
|
||||
}
|
||||
|
||||
public void handleResponse(Channel channel, Message.Response r)
|
||||
{
|
||||
try
|
||||
{
|
||||
Envelope cloned = r.getSource().clone();
|
||||
r.getSource().release();
|
||||
r.setSource(cloned);
|
||||
|
||||
if (r instanceof EventMessage)
|
||||
{
|
||||
if (eventHandler != null)
|
||||
|
|
@ -340,4 +638,139 @@ public class SimpleClient implements Closeable
|
|||
ctx.fireExceptionCaught(cause);
|
||||
}
|
||||
}
|
||||
|
||||
// Simple stand-in for Flusher for use in test code. Writers push CQL messages onto a queue and
|
||||
// this collates them into frames and flushes them to the channel.
|
||||
// Can be either scheduled to run on an EventExecutor or fired manually. If calling maybeWrite manually,
|
||||
// as SimpleClient itself does, the call must be made on the event loop.
|
||||
public static class SimpleFlusher
|
||||
{
|
||||
private static final ChannelFuture[] EMPTY_FUTURES_ARRAY = new ChannelFuture[0];
|
||||
final Queue<Envelope> outbound = new ConcurrentLinkedQueue<>();
|
||||
final FrameEncoder frameEncoder;
|
||||
private final AtomicBoolean scheduled = new AtomicBoolean(false);
|
||||
|
||||
SimpleFlusher(FrameEncoder frameEncoder)
|
||||
{
|
||||
this.frameEncoder = frameEncoder;
|
||||
}
|
||||
|
||||
public void enqueue(Envelope message)
|
||||
{
|
||||
outbound.offer(message);
|
||||
}
|
||||
|
||||
public void releaseAll()
|
||||
{
|
||||
Envelope e;
|
||||
while ((e = outbound.poll()) != null)
|
||||
e.release();
|
||||
}
|
||||
|
||||
public void schedule(ChannelHandlerContext ctx)
|
||||
{
|
||||
if (scheduled.compareAndSet(false, true))
|
||||
ctx.executor().scheduleAtFixedRate(() -> maybeWrite(ctx, ctx.voidPromise()),
|
||||
10, 10, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public void maybeWrite(ChannelHandlerContext ctx, Promise<Void> promise)
|
||||
{
|
||||
if (outbound.isEmpty())
|
||||
{
|
||||
promise.setSuccess(null);
|
||||
return;
|
||||
}
|
||||
|
||||
PromiseCombiner combiner = new PromiseCombiner(ctx.executor());
|
||||
List<Envelope> buffer = new ArrayList<>();
|
||||
long bufferSize = 0L;
|
||||
boolean pending = false;
|
||||
Envelope f;
|
||||
while ((f = outbound.poll()) != null)
|
||||
{
|
||||
if (f.header.bodySizeInBytes > MAX_FRAMED_PAYLOAD_SIZE)
|
||||
{
|
||||
combiner.addAll(writeLargeMessage(ctx, f));
|
||||
}
|
||||
else
|
||||
{
|
||||
int messageSize = envelopeSize(f.header);
|
||||
if (bufferSize + messageSize >= MAX_FRAMED_PAYLOAD_SIZE)
|
||||
{
|
||||
combiner.add(flushBuffer(ctx, buffer, bufferSize));
|
||||
buffer.clear();
|
||||
bufferSize = 0;
|
||||
}
|
||||
buffer.add(f);
|
||||
bufferSize += messageSize;
|
||||
pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (pending)
|
||||
combiner.add(flushBuffer(ctx, buffer, bufferSize));
|
||||
combiner.finish(promise);
|
||||
}
|
||||
|
||||
private ChannelFuture flushBuffer(ChannelHandlerContext ctx, List<Envelope> messages, long bufferSize)
|
||||
{
|
||||
FrameEncoder.Payload payload = allocate(Ints.checkedCast(bufferSize), true);
|
||||
|
||||
for (Envelope e : messages)
|
||||
e.encodeInto(payload.buffer);
|
||||
|
||||
payload.finish();
|
||||
ChannelPromise release = AsyncChannelPromise.withListener(ctx, future -> {
|
||||
for (Envelope e : messages)
|
||||
e.release();
|
||||
});
|
||||
return ctx.writeAndFlush(payload, release);
|
||||
}
|
||||
|
||||
private FrameEncoder.Payload allocate(int size, boolean selfContained)
|
||||
{
|
||||
FrameEncoder.Payload payload = frameEncoder.allocator()
|
||||
.allocate(selfContained, Math.min(size, MAX_FRAMED_PAYLOAD_SIZE));
|
||||
if (size >= MAX_FRAMED_PAYLOAD_SIZE)
|
||||
payload.buffer.limit(MAX_FRAMED_PAYLOAD_SIZE);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
private ChannelFuture[] writeLargeMessage(ChannelHandlerContext ctx, Envelope f)
|
||||
{
|
||||
List<ChannelFuture> futures = new ArrayList<>();
|
||||
FrameEncoder.Payload payload;
|
||||
ByteBuffer buf;
|
||||
boolean firstFrame = true;
|
||||
while (f.body.readableBytes() > 0 || firstFrame)
|
||||
{
|
||||
int payloadSize = Math.min(f.body.readableBytes(), MAX_FRAMED_PAYLOAD_SIZE);
|
||||
payload = allocate(f.body.readableBytes(), false);
|
||||
|
||||
buf = payload.buffer;
|
||||
// BufferPool may give us a buffer larger than we asked for.
|
||||
// FrameEncoder may object if buffer.remaining is >= MAX_SIZE.
|
||||
if (payloadSize >= MAX_FRAMED_PAYLOAD_SIZE)
|
||||
buf.limit(MAX_FRAMED_PAYLOAD_SIZE);
|
||||
|
||||
if (firstFrame)
|
||||
{
|
||||
f.encodeHeaderInto(buf);
|
||||
firstFrame = false;
|
||||
}
|
||||
|
||||
int remaining = Math.min(buf.remaining(), f.body.readableBytes());
|
||||
if (remaining > 0)
|
||||
buf.put(f.body.slice(f.body.readerIndex(), remaining).nioBuffer());
|
||||
|
||||
f.body.readerIndex(f.body.readerIndex() + remaining);
|
||||
payload.finish();
|
||||
futures.add(ctx.writeAndFlush(payload, ctx.newPromise()));
|
||||
}
|
||||
f.release();
|
||||
return futures.toArray(EMPTY_FUTURES_ARRAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.apache.cassandra.transport.Frame;
|
||||
|
||||
public interface FrameBodyTransformer
|
||||
{
|
||||
/**
|
||||
* Accepts the input buffer representing the frame body of an incoming message and applies a transformation.
|
||||
* Example transformations include decompression and recombining checksummed chunks into a single, serialized
|
||||
* message body.
|
||||
* @param inputBuf the frame body from an inbound message
|
||||
* @return the new frame body bytes
|
||||
* @throws IOException if the transformation failed for any reason
|
||||
*/
|
||||
ByteBuf transformInbound(ByteBuf inputBuf, EnumSet<Frame.Header.Flag> flags) throws IOException;
|
||||
|
||||
/**
|
||||
* Accepts an input buffer representing the frame body of an outbound message and applies a transformation.
|
||||
* Example transformations include compression and splitting into checksummed chunks.
|
||||
|
||||
* @param inputBuf the frame body from an outgoing message
|
||||
* @return the new frame body bytes
|
||||
* @throws IOException if the transformation failed for any reason
|
||||
*/
|
||||
ByteBuf transformOutbound(ByteBuf inputBuf) throws IOException;
|
||||
|
||||
/**
|
||||
* Returns an EnumSet of the flags that should be added to the header for any message whose frame body has been
|
||||
* modified by the transformer. E.g. it may add perform chunking & checksumming to the frame body,
|
||||
* compress it, or both.
|
||||
* @return EnumSet containing the header flags to set on messages transformed
|
||||
*/
|
||||
EnumSet<Frame.Header.Flag> getOutboundHeaderFlags();
|
||||
|
||||
}
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame.checksum;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import com.google.common.collect.ImmutableTable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.transport.Frame;
|
||||
import org.apache.cassandra.transport.ProtocolException;
|
||||
import org.apache.cassandra.transport.frame.FrameBodyTransformer;
|
||||
import org.apache.cassandra.transport.frame.compress.Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.LZ4Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.SnappyCompressor;
|
||||
import org.apache.cassandra.utils.ChecksumType;
|
||||
|
||||
import static org.apache.cassandra.transport.CBUtil.readUnsignedShort;
|
||||
|
||||
/**
|
||||
* Provides a format that implements chunking and checksumming logic
|
||||
* that maybe used in conjunction with a frame Compressor if required
|
||||
* <p>
|
||||
* <strong>1.1. Checksummed/Compression Serialized Format</strong>
|
||||
* <p>
|
||||
* <pre>
|
||||
* {@code
|
||||
* 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3
|
||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Number of Compressed Chunks | Compressed Length (e1) /
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* / Compressed Length cont. (e1) | Uncompressed Length (e1) /
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Uncompressed Length cont. (e1)| Checksum of Lengths (e1) |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Checksum of Lengths cont. (e1)| Compressed Bytes (e1) +//
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Checksum (e1) ||
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Compressed Length (e2) |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Uncompressed Length (e2) |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Checksum of Lengths (e2) |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Compressed Bytes (e2) +//
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Checksum (e2) ||
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Compressed Length (en) |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Uncompressed Length (en) |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Checksum of Lengths (en) |
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Compressed Bytes (en) +//
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* | Checksum (en) ||
|
||||
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
* <p>
|
||||
* <strong>1.2. Checksum Compression Description</strong>
|
||||
* <p>
|
||||
* The entire payload is broken into n chunks each with a pair of checksums:
|
||||
* <ul>
|
||||
* <li>[int]: compressed length of serialized bytes for this chunk (e.g. the length post compression)
|
||||
* <li>[int]: expected length of the decompressed bytes (e.g. the length after decompression)
|
||||
* <li>[int]: digest of decompressed and compressed length components above
|
||||
* <li>[k bytes]: compressed payload for this chunk
|
||||
* <li>[int]: digest of the decompressed result of the payload above for this chunk
|
||||
* </ul>
|
||||
* <p>
|
||||
*/
|
||||
public class ChecksummingTransformer implements FrameBodyTransformer
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(ChecksummingTransformer.class);
|
||||
|
||||
private static final EnumSet<Frame.Header.Flag> CHECKSUMS_ONLY = EnumSet.of(Frame.Header.Flag.CHECKSUMMED);
|
||||
private static final EnumSet<Frame.Header.Flag> CHECKSUMS_AND_COMPRESSION = EnumSet.of(Frame.Header.Flag.CHECKSUMMED, Frame.Header.Flag.COMPRESSED);
|
||||
|
||||
private static final int CHUNK_HEADER_OVERHEAD = Integer.BYTES + Integer.BYTES + Integer.BYTES + Integer.BYTES;
|
||||
|
||||
private static final ChecksummingTransformer CRC32_NO_COMPRESSION = new ChecksummingTransformer(ChecksumType.CRC32, null);
|
||||
private static final ChecksummingTransformer ADLER32_NO_COMPRESSION = new ChecksummingTransformer(ChecksumType.ADLER32, null);
|
||||
private static final ImmutableTable<ChecksumType, Compressor, ChecksummingTransformer> transformers;
|
||||
static
|
||||
{
|
||||
ImmutableTable.Builder<ChecksumType, Compressor, ChecksummingTransformer> builder = ImmutableTable.builder();
|
||||
builder.put(ChecksumType.CRC32, LZ4Compressor.INSTANCE, new ChecksummingTransformer(ChecksumType.CRC32, LZ4Compressor.INSTANCE));
|
||||
builder.put(ChecksumType.CRC32, SnappyCompressor.INSTANCE, new ChecksummingTransformer(ChecksumType.CRC32, SnappyCompressor.INSTANCE));
|
||||
builder.put(ChecksumType.ADLER32, LZ4Compressor.INSTANCE, new ChecksummingTransformer(ChecksumType.ADLER32, LZ4Compressor.INSTANCE));
|
||||
builder.put(ChecksumType.ADLER32, SnappyCompressor.INSTANCE, new ChecksummingTransformer(ChecksumType.ADLER32, SnappyCompressor.INSTANCE));
|
||||
transformers = builder.build();
|
||||
}
|
||||
|
||||
private final int blockSize;
|
||||
private final Compressor compressor;
|
||||
private final ChecksumType checksum;
|
||||
|
||||
public static ChecksummingTransformer getTransformer(ChecksumType checksumType, Compressor compressor)
|
||||
{
|
||||
ChecksummingTransformer transformer = compressor == null
|
||||
? checksumType == ChecksumType.CRC32 ? CRC32_NO_COMPRESSION : ADLER32_NO_COMPRESSION
|
||||
: transformers.get(checksumType, compressor);
|
||||
|
||||
if (transformer == null)
|
||||
{
|
||||
logger.warn("Invalid compression/checksum options supplied. %s / %s", checksumType, compressor.getClass().getName());
|
||||
throw new RuntimeException("Invalid compression / checksum options supplied");
|
||||
}
|
||||
|
||||
return transformer;
|
||||
}
|
||||
|
||||
ChecksummingTransformer(ChecksumType checksumType, Compressor compressor)
|
||||
{
|
||||
this(checksumType, DatabaseDescriptor.getNativeTransportFrameBlockSize(), compressor);
|
||||
}
|
||||
|
||||
ChecksummingTransformer(ChecksumType checksumType, int blockSize, Compressor compressor)
|
||||
{
|
||||
this.checksum = checksumType;
|
||||
this.blockSize = blockSize;
|
||||
this.compressor = compressor;
|
||||
}
|
||||
|
||||
public EnumSet<Frame.Header.Flag> getOutboundHeaderFlags()
|
||||
{
|
||||
return null == compressor ? CHECKSUMS_ONLY : CHECKSUMS_AND_COMPRESSION;
|
||||
}
|
||||
|
||||
public ByteBuf transformOutbound(ByteBuf inputBuf)
|
||||
{
|
||||
// be pessimistic about life and assume the compressed output will be the same size as the input bytes
|
||||
int maxTotalCompressedLength = maxCompressedLength(inputBuf.readableBytes());
|
||||
int expectedChunks = (int) Math.ceil((double) maxTotalCompressedLength / blockSize);
|
||||
int expectedMaxSerializedLength = Short.BYTES + (expectedChunks * CHUNK_HEADER_OVERHEAD) + maxTotalCompressedLength;
|
||||
byte[] retBuf = new byte[expectedMaxSerializedLength];
|
||||
ByteBuf ret = Unpooled.wrappedBuffer(retBuf);
|
||||
ret.writerIndex(0);
|
||||
ret.readerIndex(0);
|
||||
|
||||
// write out bogus short to start with as we'll encode one at the end
|
||||
// when we finalize the number of compressed chunks to expect and this
|
||||
// sets the writer index correctly for starting the first chunk
|
||||
ret.writeShort((short) 0);
|
||||
|
||||
byte[] inBuf = new byte[blockSize];
|
||||
byte[] outBuf = new byte[maxCompressedLength(blockSize)];
|
||||
byte[] chunkLengths = new byte[8];
|
||||
|
||||
int numCompressedChunks = 0;
|
||||
int readableBytes;
|
||||
int lengthsChecksum;
|
||||
while ((readableBytes = inputBuf.readableBytes()) > 0)
|
||||
{
|
||||
int lengthToRead = Math.min(blockSize, readableBytes);
|
||||
inputBuf.readBytes(inBuf, 0, lengthToRead);
|
||||
int uncompressedChunkChecksum = (int) checksum.of(inBuf, 0, lengthToRead);
|
||||
int compressedSize = maybeCompress(inBuf, lengthToRead, outBuf);
|
||||
|
||||
if (compressedSize < lengthToRead)
|
||||
{
|
||||
// there was some benefit to compression so write out the compressed
|
||||
// and uncompressed sizes of the chunk
|
||||
ret.writeInt(compressedSize);
|
||||
ret.writeInt(lengthToRead);
|
||||
putInt(compressedSize, chunkLengths, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// if no compression was possible, there's no need to write two lengths, so
|
||||
// just write the size of the original content (or block size), with its
|
||||
// sign flipped to signal no compression.
|
||||
ret.writeInt(-lengthToRead);
|
||||
putInt(-lengthToRead, chunkLengths, 0);
|
||||
}
|
||||
|
||||
putInt(lengthToRead, chunkLengths, 4);
|
||||
|
||||
// calculate the checksum of the compressed and decompressed lengths
|
||||
// protect us against a bogus length causing potential havoc on deserialization
|
||||
lengthsChecksum = (int) checksum.of(chunkLengths, 0, chunkLengths.length);
|
||||
ret.writeInt(lengthsChecksum);
|
||||
|
||||
// figure out how many actual bytes we're going to write and make sure we have capacity
|
||||
int toWrite = Math.min(compressedSize, lengthToRead);
|
||||
if (ret.writableBytes() < (CHUNK_HEADER_OVERHEAD + toWrite))
|
||||
{
|
||||
// this really shouldn't ever happen -- it means we either mis-calculated the number of chunks we
|
||||
// expected to create, we gave some input to the compressor that caused the output to be much
|
||||
// larger than the input.. or some other edge condition. Regardless -- resize if necessary.
|
||||
byte[] resizedRetBuf = new byte[(retBuf.length + (CHUNK_HEADER_OVERHEAD + toWrite)) * 3 / 2];
|
||||
System.arraycopy(retBuf, 0, resizedRetBuf, 0, retBuf.length);
|
||||
retBuf = resizedRetBuf;
|
||||
ByteBuf resizedRetByteBuf = Unpooled.wrappedBuffer(retBuf);
|
||||
resizedRetByteBuf.writerIndex(ret.writerIndex());
|
||||
ret = resizedRetByteBuf;
|
||||
}
|
||||
|
||||
// write the bytes, either compressed or uncompressed
|
||||
if (compressedSize < lengthToRead)
|
||||
ret.writeBytes(outBuf, 0, toWrite); // compressed
|
||||
else
|
||||
ret.writeBytes(inBuf, 0, toWrite); // uncompressed
|
||||
|
||||
// checksum of the uncompressed chunk
|
||||
ret.writeInt(uncompressedChunkChecksum);
|
||||
|
||||
numCompressedChunks++;
|
||||
}
|
||||
|
||||
// now update the number of chunks
|
||||
ret.setShort(0, (short) numCompressedChunks);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public ByteBuf transformInbound(ByteBuf inputBuf, EnumSet<Frame.Header.Flag> flags)
|
||||
{
|
||||
int numChunks = readUnsignedShort(inputBuf);
|
||||
|
||||
int currentPosition = 0;
|
||||
int decompressedLength;
|
||||
int lengthsChecksum;
|
||||
|
||||
byte[] buf = null;
|
||||
byte[] retBuf = new byte[inputBuf.readableBytes()];
|
||||
byte[] chunkLengths = new byte[8];
|
||||
for (int i = 0; i < numChunks; i++)
|
||||
{
|
||||
int compressedLength = inputBuf.readInt();
|
||||
// if the input was actually compressed, then the writer should have written a decompressed
|
||||
// length. If not, then we can infer that the compressed length has had its sign bit flipped
|
||||
// and can derive the decompressed length from that
|
||||
decompressedLength = compressedLength >= 0 ? inputBuf.readInt() : Math.abs(compressedLength);
|
||||
|
||||
putInt(compressedLength, chunkLengths, 0);
|
||||
putInt(decompressedLength, chunkLengths, 4);
|
||||
lengthsChecksum = inputBuf.readInt();
|
||||
// calculate checksum on lengths (decompressed and compressed) and make sure it matches
|
||||
int calculatedLengthsChecksum = (int) checksum.of(chunkLengths, 0, chunkLengths.length);
|
||||
if (lengthsChecksum != calculatedLengthsChecksum)
|
||||
{
|
||||
throw new ProtocolException(String.format("Checksum invalid on chunk bytes lengths. Deserialized compressed " +
|
||||
"length: %d decompressed length: %d. %d != %d", compressedLength,
|
||||
decompressedLength, lengthsChecksum, calculatedLengthsChecksum));
|
||||
}
|
||||
|
||||
// do we have enough space in the decompression buffer?
|
||||
if (currentPosition + decompressedLength > retBuf.length)
|
||||
{
|
||||
byte[] resizedBuf = new byte[retBuf.length + decompressedLength * 3 / 2];
|
||||
System.arraycopy(retBuf, 0, resizedBuf, 0, retBuf.length);
|
||||
retBuf = resizedBuf;
|
||||
}
|
||||
|
||||
// now we've validated the lengths checksum, we can abs the compressed length
|
||||
// to figure out the actual number of bytes we're going to read
|
||||
int toRead = Math.abs(compressedLength);
|
||||
if (buf == null || buf.length < toRead)
|
||||
buf = new byte[toRead];
|
||||
|
||||
// get the (possibly) compressed bytes for this chunk
|
||||
inputBuf.readBytes(buf, 0, toRead);
|
||||
|
||||
// decompress using the original compressed length, so it's a no-op if that's < 0
|
||||
byte[] decompressedChunk = maybeDecompress(buf, compressedLength, decompressedLength, flags);
|
||||
|
||||
// add the decompressed bytes into the ret buf
|
||||
System.arraycopy(decompressedChunk, 0, retBuf, currentPosition, decompressedLength);
|
||||
currentPosition += decompressedLength;
|
||||
|
||||
// get the checksum of the original source bytes and compare against what we read
|
||||
int expectedDecompressedChecksum = inputBuf.readInt();
|
||||
int calculatedDecompressedChecksum = (int) checksum.of(decompressedChunk, 0, decompressedLength);
|
||||
if (expectedDecompressedChecksum != calculatedDecompressedChecksum)
|
||||
{
|
||||
throw new ProtocolException("Decompressed checksum for chunk does not match expected checksum");
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuf ret = Unpooled.wrappedBuffer(retBuf, 0, currentPosition);
|
||||
ret.writerIndex(currentPosition);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private int maxCompressedLength(int uncompressedLength)
|
||||
{
|
||||
return null == compressor ? uncompressedLength : compressor.maxCompressedLength(uncompressedLength);
|
||||
|
||||
}
|
||||
|
||||
private int maybeCompress(byte[] input, int length, byte[] output)
|
||||
{
|
||||
if (null == compressor)
|
||||
{
|
||||
System.arraycopy(input, 0, output, 0, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return compressor.compress(input, 0, length, output, 0);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
logger.info("IO error during compression of frame body chunk", e);
|
||||
throw new ProtocolException("Error compressing frame body chunk");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] maybeDecompress(byte[] input, int length, int expectedLength, EnumSet<Frame.Header.Flag> flags)
|
||||
{
|
||||
if (null == compressor || !flags.contains(Frame.Header.Flag.COMPRESSED) || length < 0)
|
||||
return input;
|
||||
|
||||
try
|
||||
{
|
||||
return compressor.decompress(input, 0, length, expectedLength);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
logger.info("IO error during decompression of frame body chunk", e);
|
||||
throw new ProtocolException("Error decompressing frame body chunk");
|
||||
}
|
||||
}
|
||||
|
||||
private void putInt(int val, byte[] dest, int offset)
|
||||
{
|
||||
dest[offset] = (byte) (val >>> 24);
|
||||
dest[offset + 1] = (byte) (val >>> 16);
|
||||
dest[offset + 2] = (byte) (val >>> 8);
|
||||
dest[offset + 3] = (byte) (val);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame.compress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.apache.cassandra.transport.CBUtil;
|
||||
import org.apache.cassandra.transport.Frame;
|
||||
import org.apache.cassandra.transport.ProtocolException;
|
||||
import org.apache.cassandra.transport.frame.FrameBodyTransformer;
|
||||
|
||||
public abstract class CompressingTransformer implements FrameBodyTransformer
|
||||
{
|
||||
private static final CompressingTransformer LZ4 = new LZ4();
|
||||
private static final CompressingTransformer SNAPPY = new Snappy();
|
||||
|
||||
private static final EnumSet<Frame.Header.Flag> headerFlags = EnumSet.of(Frame.Header.Flag.COMPRESSED);
|
||||
|
||||
public static final CompressingTransformer getTransformer(Compressor compressor)
|
||||
{
|
||||
if (compressor instanceof LZ4Compressor)
|
||||
return LZ4;
|
||||
|
||||
if (compressor instanceof SnappyCompressor)
|
||||
{
|
||||
if (SnappyCompressor.INSTANCE == null)
|
||||
throw new ProtocolException("This instance does not support Snappy compression");
|
||||
|
||||
return SNAPPY;
|
||||
}
|
||||
|
||||
throw new ProtocolException("Unsupported compression implementation: " + compressor.getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
CompressingTransformer() {}
|
||||
|
||||
public EnumSet<Frame.Header.Flag> getOutboundHeaderFlags()
|
||||
{
|
||||
return headerFlags;
|
||||
}
|
||||
|
||||
public ByteBuf transformInbound(ByteBuf inputBuf, EnumSet<Frame.Header.Flag> flags) throws IOException
|
||||
{
|
||||
return transformInbound(inputBuf);
|
||||
}
|
||||
|
||||
abstract ByteBuf transformInbound(ByteBuf inputBuf) throws IOException;
|
||||
|
||||
// Simple LZ4 encoding prefixes the compressed bytes with the
|
||||
// length of the uncompressed bytes. This length is explicitly big-endian
|
||||
// as the native protocol is entirely big-endian, so it feels like putting
|
||||
// little-endian here would be a annoying trap for client writer
|
||||
private static class LZ4 extends CompressingTransformer
|
||||
{
|
||||
public ByteBuf transformOutbound(ByteBuf inputBuf) throws IOException
|
||||
{
|
||||
byte[] input = CBUtil.readRawBytes(inputBuf);
|
||||
int maxCompressedLength = LZ4Compressor.INSTANCE.maxCompressedLength(input.length);
|
||||
ByteBuf outputBuf = CBUtil.allocator.heapBuffer(Integer.BYTES + maxCompressedLength);
|
||||
byte[] output = outputBuf.array();
|
||||
int outputOffset = outputBuf.arrayOffset();
|
||||
output[outputOffset] = (byte) (input.length >>> 24);
|
||||
output[outputOffset + 1] = (byte) (input.length >>> 16);
|
||||
output[outputOffset + 2] = (byte) (input.length >>> 8);
|
||||
output[outputOffset + 3] = (byte) (input.length);
|
||||
try
|
||||
{
|
||||
int written = LZ4Compressor.INSTANCE.compress(input, 0, input.length, output, Integer.BYTES + outputOffset);
|
||||
outputBuf.writerIndex(Integer.BYTES + written);
|
||||
return outputBuf;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
outputBuf.release();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuf transformInbound(ByteBuf inputBuf) throws IOException
|
||||
{
|
||||
byte[] input = CBUtil.readRawBytes(inputBuf);
|
||||
int uncompressedLength = ((input[0] & 0xFF) << 24)
|
||||
| ((input[1] & 0xFF) << 16)
|
||||
| ((input[2] & 0xFF) << 8)
|
||||
| ((input[3] & 0xFF));
|
||||
ByteBuf outputBuf = CBUtil.allocator.heapBuffer(uncompressedLength);
|
||||
try
|
||||
{
|
||||
outputBuf.writeBytes(LZ4Compressor.INSTANCE.decompress(input,
|
||||
Integer.BYTES,
|
||||
input.length - Integer.BYTES,
|
||||
uncompressedLength));
|
||||
return outputBuf;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
outputBuf.release();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple Snappy encoding simply writes the compressed bytes, without the preceding length
|
||||
private static class Snappy extends CompressingTransformer
|
||||
{
|
||||
public ByteBuf transformOutbound(ByteBuf inputBuf) throws IOException
|
||||
{
|
||||
byte[] input = CBUtil.readRawBytes(inputBuf);
|
||||
int uncompressedLength = input.length;
|
||||
int maxCompressedLength = SnappyCompressor.INSTANCE.maxCompressedLength(uncompressedLength);
|
||||
ByteBuf outputBuf = CBUtil.allocator.heapBuffer(maxCompressedLength);
|
||||
try
|
||||
{
|
||||
int written = SnappyCompressor.INSTANCE.compress(input,
|
||||
0,
|
||||
uncompressedLength,
|
||||
outputBuf.array(),
|
||||
outputBuf.arrayOffset());
|
||||
outputBuf.writerIndex(written);
|
||||
return outputBuf;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
outputBuf.release();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuf transformInbound(ByteBuf inputBuf) throws IOException
|
||||
{
|
||||
byte[] input = CBUtil.readRawBytes(inputBuf);
|
||||
int uncompressedLength = org.xerial.snappy.Snappy.uncompressedLength(input);
|
||||
ByteBuf outputBuf = CBUtil.allocator.heapBuffer(uncompressedLength);
|
||||
try
|
||||
{
|
||||
outputBuf.writeBytes(SnappyCompressor.INSTANCE.decompress(input, 0, input.length, uncompressedLength));
|
||||
return outputBuf;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
outputBuf.release();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame.compress;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Analogous to {@link org.apache.cassandra.io.compress.ICompressor}, but different enough that
|
||||
* it's worth specializing:
|
||||
* <ul>
|
||||
* <li>disk IO is mostly oriented around ByteBuffers, whereas with Frames raw byte arrays are
|
||||
* primarily used </li>
|
||||
* <li>our LZ4 compression format is opionated about the endianness of the preceding length
|
||||
* bytes, big for protocol, little for disk</li>
|
||||
* <li>ICompressor doesn't make it easy to pre-allocate the output buffer/array</li>
|
||||
* </ul>
|
||||
*
|
||||
* In future it may be worth revisiting to unify the interfaces.
|
||||
*/
|
||||
public interface Compressor
|
||||
{
|
||||
/**
|
||||
* @param length the decompressed length being compressed
|
||||
* @return the maximum length output possible for an input of the provided length
|
||||
*/
|
||||
int maxCompressedLength(int length);
|
||||
|
||||
/**
|
||||
* @param src the input bytes to be compressed
|
||||
* @param srcOffset the offset to start compressing src from
|
||||
* @param length the total number of bytes from srcOffset to pass to the compressor implementation
|
||||
* @param dest the output buffer to write the compressed bytes to
|
||||
* @param destOffset the offset into the dest buffer to start writing the compressed bytes
|
||||
* @return the length of resulting compressed bytes written into the dest buffer
|
||||
* @throws IOException if the compression implementation failed while compressing the input bytes
|
||||
*/
|
||||
int compress(byte[] src, int srcOffset, int length, byte[] dest, int destOffset) throws IOException;
|
||||
|
||||
/**
|
||||
* @param src the compressed bytes to be decompressed
|
||||
* @param expectedDecompressedLength the expected length the input bytes will decompress to
|
||||
* @return a byte[] containing the resuling decompressed bytes
|
||||
* @throws IOException thrown if the compression implementation failed to decompress the provided input bytes
|
||||
*/
|
||||
byte[] decompress(byte[] src, int srcOffset, int length, int expectedDecompressedLength) throws IOException;
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame.compress;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import net.jpountz.lz4.LZ4Factory;
|
||||
import net.jpountz.lz4.LZ4SafeDecompressor;
|
||||
|
||||
public class LZ4Compressor implements Compressor
|
||||
{
|
||||
public static final LZ4Compressor INSTANCE = new LZ4Compressor();
|
||||
|
||||
private final net.jpountz.lz4.LZ4Compressor compressor;
|
||||
private final LZ4SafeDecompressor decompressor;
|
||||
|
||||
private LZ4Compressor()
|
||||
{
|
||||
final LZ4Factory lz4Factory = LZ4Factory.fastestInstance();
|
||||
compressor = lz4Factory.fastCompressor();
|
||||
decompressor = lz4Factory.safeDecompressor();
|
||||
}
|
||||
|
||||
public int maxCompressedLength(int length)
|
||||
{
|
||||
return compressor.maxCompressedLength(length);
|
||||
}
|
||||
|
||||
public int compress(byte[] src, int srcOffset, int length, byte[] dest, int destOffset) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
return compressor.compress(src, srcOffset, length, dest, destOffset);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
throw new IOException("Error caught during LZ4 compression", t);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decompress(byte[] src, int offset, int length, int expectedDecompressedLength) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] decompressed = new byte[expectedDecompressedLength];
|
||||
decompressor.decompress(src, offset, length, decompressed, 0, expectedDecompressedLength);
|
||||
return decompressed;
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
throw new IOException("Error caught during LZ4 decompression", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame.compress;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
import org.xerial.snappy.Snappy;
|
||||
import org.xerial.snappy.SnappyError;
|
||||
|
||||
public class SnappyCompressor implements Compressor
|
||||
{
|
||||
public static final SnappyCompressor INSTANCE;
|
||||
static
|
||||
{
|
||||
SnappyCompressor i;
|
||||
try
|
||||
{
|
||||
i = new SnappyCompressor();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
JVMStabilityInspector.inspectThrowable(e);
|
||||
i = null;
|
||||
}
|
||||
catch (NoClassDefFoundError | SnappyError | UnsatisfiedLinkError e)
|
||||
{
|
||||
i = null;
|
||||
}
|
||||
INSTANCE = i;
|
||||
}
|
||||
|
||||
private SnappyCompressor()
|
||||
{
|
||||
// this would throw java.lang.NoClassDefFoundError if Snappy class
|
||||
// wasn't found at runtime which should be processed by the calling method
|
||||
Snappy.getNativeLibraryVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int maxCompressedLength(int length)
|
||||
{
|
||||
return Snappy.maxCompressedLength(length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compress(byte[] src, int srcOffset, int length, byte[] dest, int destOffset) throws IOException
|
||||
{
|
||||
return Snappy.compress(src, 0, src.length, dest, destOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decompress(byte[] src, int offset, int length, int expectedDecompressedLength) throws IOException
|
||||
{
|
||||
if (!Snappy.isValidCompressedBuffer(src, 0, length))
|
||||
throw new IOException("Provided frame does not appear to be Snappy compressed");
|
||||
|
||||
int uncompressedLength = Snappy.uncompressedLength(src);
|
||||
byte[] output = new byte[uncompressedLength];
|
||||
Snappy.uncompress(src, offset, length, output, 0);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,10 +26,9 @@ import io.netty.buffer.ByteBuf;
|
|||
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.Compressor;
|
||||
import org.apache.cassandra.transport.Message;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
import org.apache.cassandra.transport.frame.compress.SnappyCompressor;
|
||||
import org.apache.cassandra.utils.ChecksumType;
|
||||
|
||||
/**
|
||||
* Message to indicate that the server is ready to receive requests.
|
||||
|
|
@ -61,29 +60,20 @@ public class OptionsMessage extends Message.Request
|
|||
@Override
|
||||
protected Message.Response execute(QueryState state, long queryStartNanoTime, boolean traceRequest)
|
||||
{
|
||||
List<String> cqlVersions = new ArrayList<>();
|
||||
List<String> cqlVersions = new ArrayList<String>();
|
||||
cqlVersions.add(QueryProcessor.CQL_VERSION.toString());
|
||||
|
||||
List<String> compressions = new ArrayList<>();
|
||||
if (SnappyCompressor.INSTANCE != null)
|
||||
List<String> compressions = new ArrayList<String>();
|
||||
if (Compressor.SnappyCompressor.instance != null)
|
||||
compressions.add("snappy");
|
||||
// LZ4 is always available since worst case scenario it default to a pure JAVA implem.
|
||||
compressions.add("lz4");
|
||||
|
||||
Map<String, List<String>> supported = new HashMap<>();
|
||||
Map<String, List<String>> supported = new HashMap<String, List<String>>();
|
||||
supported.put(StartupMessage.CQL_VERSION, cqlVersions);
|
||||
supported.put(StartupMessage.COMPRESSION, compressions);
|
||||
supported.put(StartupMessage.PROTOCOL_VERSIONS, ProtocolVersion.supportedVersions());
|
||||
|
||||
if (connection.getVersion().supportsChecksums())
|
||||
{
|
||||
ChecksumType[] types = ChecksumType.values();
|
||||
List<String> checksumImpls = new ArrayList<>(types.length);
|
||||
for (ChecksumType type : types)
|
||||
checksumImpls.add(type.toString());
|
||||
supported.put(StartupMessage.CHECKSUM, checksumImpls);
|
||||
}
|
||||
|
||||
return new SupportedMessage(supported);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,13 +26,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
|
|||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.*;
|
||||
import org.apache.cassandra.transport.frame.checksum.ChecksummingTransformer;
|
||||
import org.apache.cassandra.transport.frame.compress.CompressingTransformer;
|
||||
import org.apache.cassandra.transport.frame.compress.Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.LZ4Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.SnappyCompressor;
|
||||
import org.apache.cassandra.utils.CassandraVersion;
|
||||
import org.apache.cassandra.utils.ChecksumType;
|
||||
|
||||
/**
|
||||
* The initial message of the protocol.
|
||||
|
|
@ -45,7 +39,6 @@ public class StartupMessage extends Message.Request
|
|||
public static final String PROTOCOL_VERSIONS = "PROTOCOL_VERSIONS";
|
||||
public static final String DRIVER_NAME = "DRIVER_NAME";
|
||||
public static final String DRIVER_VERSION = "DRIVER_VERSION";
|
||||
public static final String CHECKSUM = "CONTENT_CHECKSUM";
|
||||
public static final String THROW_ON_OVERLOAD = "THROW_ON_OVERLOAD";
|
||||
|
||||
public static final Message.Codec<StartupMessage> codec = new Message.Codec<StartupMessage>()
|
||||
|
|
@ -91,18 +84,27 @@ public class StartupMessage extends Message.Request
|
|||
throw new ProtocolException(e.getMessage());
|
||||
}
|
||||
|
||||
ChecksumType checksumType = getChecksumType();
|
||||
Compressor compressor = getCompressor();
|
||||
if (options.containsKey(COMPRESSION))
|
||||
{
|
||||
String compression = options.get(COMPRESSION).toLowerCase();
|
||||
if (compression.equals("snappy"))
|
||||
{
|
||||
if (Compressor.SnappyCompressor.instance == null)
|
||||
throw new ProtocolException("This instance does not support Snappy compression");
|
||||
|
||||
if (null != checksumType)
|
||||
{
|
||||
if (!connection.getVersion().supportsChecksums())
|
||||
throw new ProtocolException(String.format("Invalid message flag. Protocol version %s does not support frame body checksums", connection.getVersion().toString()));
|
||||
connection.setTransformer(ChecksummingTransformer.getTransformer(checksumType, compressor));
|
||||
}
|
||||
else if (null != compressor)
|
||||
{
|
||||
connection.setTransformer(CompressingTransformer.getTransformer(compressor));
|
||||
if (getSource().header.version.isGreaterOrEqualTo(ProtocolVersion.V5))
|
||||
throw new ProtocolException("Snappy compression is not supported in protocol V5");
|
||||
|
||||
connection.setCompressor(Compressor.SnappyCompressor.instance);
|
||||
}
|
||||
else if (compression.equals("lz4"))
|
||||
{
|
||||
connection.setCompressor(Compressor.LZ4Compressor.instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ProtocolException(String.format("Unknown compression algorithm: %s", compression));
|
||||
}
|
||||
}
|
||||
|
||||
connection.setThrowOnOverload("1".equals(options.get(THROW_ON_OVERLOAD)));
|
||||
|
|
@ -129,42 +131,6 @@ public class StartupMessage extends Message.Request
|
|||
return newMap;
|
||||
}
|
||||
|
||||
private ChecksumType getChecksumType() throws ProtocolException
|
||||
{
|
||||
String name = options.get(CHECKSUM);
|
||||
try
|
||||
{
|
||||
return name != null ? ChecksumType.valueOf(name.toUpperCase()) : null;
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw new ProtocolException(String.format("Requested checksum type %s is not known or supported by " +
|
||||
"this version of Cassandra", name));
|
||||
}
|
||||
}
|
||||
|
||||
private Compressor getCompressor() throws ProtocolException
|
||||
{
|
||||
String name = options.get(COMPRESSION);
|
||||
if (null == name)
|
||||
return null;
|
||||
|
||||
switch (name.toLowerCase())
|
||||
{
|
||||
case "snappy":
|
||||
{
|
||||
if (SnappyCompressor.INSTANCE == null)
|
||||
throw new ProtocolException("This instance does not support Snappy compression");
|
||||
|
||||
return SnappyCompressor.INSTANCE;
|
||||
}
|
||||
case "lz4":
|
||||
return LZ4Compressor.INSTANCE;
|
||||
default:
|
||||
throw new ProtocolException(String.format("Unknown compression algorithm: %s", name));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.datastax.driver.core.SimpleStatement;
|
||||
import org.apache.cassandra.cql3.ColumnIdentifier;
|
||||
import org.apache.cassandra.cql3.ColumnSpecification;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.cql3.ResultSet;
|
||||
import org.apache.cassandra.db.ConsistencyLevel;
|
||||
import org.apache.cassandra.db.marshal.BytesType;
|
||||
import org.apache.cassandra.net.AbstractMessageHandler;
|
||||
import org.apache.cassandra.net.ResourceLimits;
|
||||
import org.apache.cassandra.transport.messages.QueryMessage;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
public class BurnTestUtil
|
||||
{
|
||||
public static class SizeCaps
|
||||
{
|
||||
public final int valueMinSize;
|
||||
public final int valueMaxSize;
|
||||
public final int columnCountCap;
|
||||
public final int rowsCountCap;
|
||||
|
||||
public SizeCaps(int valueMinSize, int valueMaxSize, int columnCountCap, int rowsCountCap)
|
||||
{
|
||||
this.valueMinSize = valueMinSize;
|
||||
this.valueMaxSize = valueMaxSize;
|
||||
this.columnCountCap = columnCountCap;
|
||||
this.rowsCountCap = rowsCountCap;
|
||||
}
|
||||
}
|
||||
|
||||
public static SimpleStatement generateQueryStatement(int idx, SizeCaps sizeCaps)
|
||||
{
|
||||
Random rnd = new Random(idx);
|
||||
|
||||
ByteBuffer[] values = new ByteBuffer[sizeCaps.columnCountCap];
|
||||
for (int i = 0; i < sizeCaps.columnCountCap; i++)
|
||||
values[i] = bytes(rnd, sizeCaps.valueMinSize, sizeCaps.valueMaxSize);
|
||||
|
||||
return new SimpleStatement(Integer.toString(idx), values);
|
||||
}
|
||||
|
||||
public static QueryMessage generateQueryMessage(int idx, SizeCaps sizeCaps)
|
||||
{
|
||||
Random rnd = new Random(idx);
|
||||
List<ByteBuffer> values = new ArrayList<>();
|
||||
for (int i = 0; i < sizeCaps.columnCountCap * sizeCaps.rowsCountCap; i++)
|
||||
values.add(bytes(rnd, sizeCaps.valueMinSize, sizeCaps.valueMaxSize));
|
||||
|
||||
QueryOptions queryOptions = QueryOptions.create(ConsistencyLevel.ONE,
|
||||
values,
|
||||
true,
|
||||
10,
|
||||
null,
|
||||
null,
|
||||
ProtocolVersion.V4,
|
||||
"KEYSPACE");
|
||||
|
||||
return new QueryMessage(Integer.toString(idx), queryOptions);
|
||||
}
|
||||
|
||||
public static ResultMessage.Rows generateRows(int idx, SizeCaps sizeCaps)
|
||||
{
|
||||
Random rnd = new Random(idx);
|
||||
List<ColumnSpecification> columns = new ArrayList<>();
|
||||
for (int i = 0; i < sizeCaps.columnCountCap; i++)
|
||||
{
|
||||
columns.add(new ColumnSpecification("ks", "cf",
|
||||
new ColumnIdentifier(bytes(rnd, 5, 10), BytesType.instance),
|
||||
BytesType.instance));
|
||||
}
|
||||
|
||||
List<List<ByteBuffer>> rows = new ArrayList<>();
|
||||
int count = rnd.nextInt(sizeCaps.rowsCountCap);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
List<ByteBuffer> row = new ArrayList<>();
|
||||
for (int j = 0; j < sizeCaps.columnCountCap; j++)
|
||||
row.add(bytes(rnd, sizeCaps.valueMinSize, sizeCaps.valueMaxSize));
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
ResultSet resultSet = new ResultSet(new ResultSet.ResultMetadata(columns), rows);
|
||||
return new ResultMessage.Rows(resultSet);
|
||||
}
|
||||
|
||||
public static ByteBuffer bytes(Random rnd, int minSize, int maxSize)
|
||||
{
|
||||
byte[] bytes = new byte[rnd.nextInt(maxSize) + minSize];
|
||||
rnd.nextBytes(bytes);
|
||||
return ByteBuffer.wrap(bytes);
|
||||
}
|
||||
|
||||
public static Function<ClientResourceLimits.Allocator, ClientResourceLimits.ResourceProvider> observableResourceProvider(final CQLConnectionTest.AllocationObserver observer)
|
||||
{
|
||||
return allocator ->
|
||||
{
|
||||
final ClientResourceLimits.ResourceProvider.Default delegate = new ClientResourceLimits.ResourceProvider.Default(allocator);
|
||||
return new ClientResourceLimits.ResourceProvider()
|
||||
{
|
||||
public ResourceLimits.Limit globalLimit()
|
||||
{
|
||||
return observer.global(delegate.globalLimit());
|
||||
}
|
||||
|
||||
public AbstractMessageHandler.WaitQueue globalWaitQueue()
|
||||
{
|
||||
return delegate.globalWaitQueue();
|
||||
}
|
||||
|
||||
public ResourceLimits.Limit endpointLimit()
|
||||
{
|
||||
return observer.endpoint(delegate.endpointLimit());
|
||||
}
|
||||
|
||||
public AbstractMessageHandler.WaitQueue endpointWaitQueue()
|
||||
{
|
||||
return delegate.endpointWaitQueue();
|
||||
}
|
||||
|
||||
public void release()
|
||||
{
|
||||
delegate.release();
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.apache.commons.math.stat.descriptive.DescriptiveStatistics;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.datastax.driver.core.*;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
import org.apache.cassandra.service.NativeTransportService;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.messages.QueryMessage;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
import org.apache.cassandra.utils.AssertUtil;
|
||||
|
||||
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.SizeCaps;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.generateQueryMessage;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.generateQueryStatement;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.generateRows;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class DriverBurnTest extends CQLTester
|
||||
{
|
||||
private final CQLConnectionTest.AllocationObserver allocationObserver = new CQLConnectionTest.AllocationObserver();
|
||||
|
||||
@Before
|
||||
public void setup()
|
||||
{
|
||||
PipelineConfigurator configurator = new PipelineConfigurator(NativeTransportService.useEpoll(), false, false, UNENCRYPTED)
|
||||
{
|
||||
protected ClientResourceLimits.ResourceProvider resourceProvider(ClientResourceLimits.Allocator allocator)
|
||||
{
|
||||
return BurnTestUtil.observableResourceProvider(allocationObserver).apply(allocator);
|
||||
}
|
||||
};
|
||||
|
||||
requireNetwork((builder) -> builder.withPipelineConfigurator(configurator));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() throws Throwable
|
||||
{
|
||||
final SizeCaps smallMessageCap = new SizeCaps(10, 20, 5, 10);
|
||||
final SizeCaps largeMessageCap = new SizeCaps(1000, 2000, 5, 150);
|
||||
int largeMessageFrequency = 1000;
|
||||
|
||||
Message.Type.QUERY.unsafeSetCodec(new Message.Codec<QueryMessage>() {
|
||||
public QueryMessage decode(ByteBuf body, ProtocolVersion version)
|
||||
{
|
||||
QueryMessage queryMessage = QueryMessage.codec.decode(body, version);
|
||||
return new QueryMessage(queryMessage.query, queryMessage.options) {
|
||||
protected Message.Response execute(QueryState state, long queryStartNanoTime, boolean traceRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
int idx = Integer.parseInt(queryMessage.query);
|
||||
SizeCaps caps = idx % largeMessageFrequency == 0 ? largeMessageCap : smallMessageCap;
|
||||
return generateRows(idx, caps);
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
// for the requests driver issues under the hood
|
||||
return super.execute(state, queryStartNanoTime, traceRequest);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void encode(QueryMessage queryMessage, ByteBuf dest, ProtocolVersion version)
|
||||
{
|
||||
QueryMessage.codec.encode(queryMessage, dest, version);
|
||||
}
|
||||
|
||||
public int encodedSize(QueryMessage queryMessage, ProtocolVersion version)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
List<AssertUtil.ThrowingSupplier<Cluster.Builder>> suppliers =
|
||||
Arrays.asList(() -> Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.withProtocolVersion(com.datastax.driver.core.ProtocolVersion.V4)
|
||||
.withPort(nativePort),
|
||||
() -> Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.allowBetaProtocolVersion()
|
||||
.withPort(nativePort),
|
||||
() -> Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.withCompression(ProtocolOptions.Compression.LZ4)
|
||||
.allowBetaProtocolVersion()
|
||||
.withPort(nativePort),
|
||||
() -> Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.withCompression(ProtocolOptions.Compression.LZ4)
|
||||
.withProtocolVersion(com.datastax.driver.core.ProtocolVersion.V4)
|
||||
.withPort(nativePort)
|
||||
);
|
||||
|
||||
int threads = 10;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threads);
|
||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
CountDownLatch signal = new CountDownLatch(1);
|
||||
|
||||
for (int t = 0; t < threads; t++)
|
||||
{
|
||||
int threadId = t;
|
||||
executor.execute(() -> {
|
||||
try (Cluster driver = suppliers.get(threadId % suppliers.size()).get().build();
|
||||
Session session = driver.connect())
|
||||
{
|
||||
int counter = 0;
|
||||
while(!Thread.interrupted())
|
||||
{
|
||||
Map<Integer, ResultSetFuture> futures = new HashMap<>();
|
||||
|
||||
for (int j = 0; j < 10; j++)
|
||||
{
|
||||
int descriptor = counter + j * 100 + threadId * 10000;
|
||||
SizeCaps caps = descriptor % largeMessageFrequency == 0 ? largeMessageCap : smallMessageCap;
|
||||
futures.put(j, session.executeAsync(generateQueryStatement(descriptor, caps)));
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, ResultSetFuture> e : futures.entrySet())
|
||||
{
|
||||
final int j = e.getKey().intValue();
|
||||
final int descriptor = counter + j * 100 + threadId * 10000;
|
||||
SizeCaps caps = descriptor % largeMessageFrequency == 0 ? largeMessageCap : smallMessageCap;
|
||||
ResultMessage.Rows expectedRS = generateRows(descriptor, caps);
|
||||
List<Row> actualRS = e.getValue().get().all();
|
||||
|
||||
for (int i = 0; i < actualRS.size(); i++)
|
||||
{
|
||||
List<ByteBuffer> expected = expectedRS.result.rows.get(i);
|
||||
Row actual = actualRS.get(i);
|
||||
|
||||
for (int col = 0; col < expected.size(); col++)
|
||||
Assert.assertEquals(actual.getBytes(col), expected.get(col));
|
||||
}
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
error.set(e);
|
||||
signal.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Assert.assertFalse(signal.await(120, TimeUnit.SECONDS));
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(allocationObserver.endpointAllocationTotal()).isEqualTo(allocationObserver.endpointReleaseTotal());
|
||||
assertThat(allocationObserver.globalAllocationTotal()).isEqualTo(allocationObserver.globalReleaseTotal());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureSmallV5() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(10, 20, 5, 10),
|
||||
new SizeCaps(10, 20, 5, 10),
|
||||
Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.allowBetaProtocolVersion()
|
||||
.withPort(nativePort));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureSmallV4() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(10, 20, 5, 10),
|
||||
new SizeCaps(10, 20, 5, 10),
|
||||
Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.withProtocolVersion(com.datastax.driver.core.ProtocolVersion.V4)
|
||||
.withPort(nativePort));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureLargeV5() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(1000, 2000, 5, 150),
|
||||
new SizeCaps(1000, 2000, 5, 150),
|
||||
Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.allowBetaProtocolVersion()
|
||||
.withPort(nativePort));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureLargeV4() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(1000, 2000, 5, 150),
|
||||
new SizeCaps(1000, 2000, 5, 150),
|
||||
Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.withProtocolVersion(com.datastax.driver.core.ProtocolVersion.V4)
|
||||
.withPort(nativePort));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureSmallV5ithCompression() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(10, 20, 5, 10),
|
||||
new SizeCaps(10, 20, 5, 10),
|
||||
Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.allowBetaProtocolVersion()
|
||||
.withCompression(ProtocolOptions.Compression.LZ4)
|
||||
.withPort(nativePort));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureSmallV4WithCompression() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(10, 20, 5, 10),
|
||||
new SizeCaps(10, 20, 5, 10),
|
||||
Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.withProtocolVersion(com.datastax.driver.core.ProtocolVersion.V4)
|
||||
.withCompression(ProtocolOptions.Compression.LZ4)
|
||||
.withPort(nativePort));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureLargeV5WithCompression() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(1000, 2000, 5, 150),
|
||||
new SizeCaps(1000, 2000, 5, 150),
|
||||
Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.allowBetaProtocolVersion()
|
||||
.withCompression(ProtocolOptions.Compression.LZ4)
|
||||
.withPort(nativePort));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureLargeV4WithCompression() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(1000, 2000, 5, 150),
|
||||
new SizeCaps(1000, 2000, 5, 150),
|
||||
Cluster.builder().addContactPoint(nativeAddr.getHostAddress())
|
||||
.withProtocolVersion(com.datastax.driver.core.ProtocolVersion.V4)
|
||||
.withCompression(ProtocolOptions.Compression.LZ4)
|
||||
.withPort(nativePort));
|
||||
}
|
||||
|
||||
public void perfTest(SizeCaps requestCaps, SizeCaps responseCaps, Cluster.Builder builder) throws Throwable
|
||||
{
|
||||
SimpleStatement request = generateQueryStatement(0, requestCaps);
|
||||
ResultMessage.Rows response = generateRows(0, responseCaps);
|
||||
QueryMessage requestMessage = generateQueryMessage(0, requestCaps);
|
||||
Envelope message = requestMessage.encode(ProtocolVersion.V4);
|
||||
int requestSize = message.body.readableBytes();
|
||||
message.release();
|
||||
message = response.encode(ProtocolVersion.V4);
|
||||
int responseSize = message.body.readableBytes();
|
||||
message.release();
|
||||
Message.Type.QUERY.unsafeSetCodec(new Message.Codec<QueryMessage>() {
|
||||
public QueryMessage decode(ByteBuf body, ProtocolVersion version)
|
||||
{
|
||||
QueryMessage queryMessage = QueryMessage.codec.decode(body, version);
|
||||
return new QueryMessage(queryMessage.query, queryMessage.options) {
|
||||
protected Message.Response execute(QueryState state, long queryStartNanoTime, boolean traceRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
int idx = Integer.parseInt(queryMessage.query); // unused
|
||||
return generateRows(idx, responseCaps);
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
// for the requests driver issues under the hood
|
||||
return super.execute(state, queryStartNanoTime, traceRequest);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void encode(QueryMessage queryMessage, ByteBuf dest, ProtocolVersion version)
|
||||
{
|
||||
QueryMessage.codec.encode(queryMessage, dest, version);
|
||||
}
|
||||
|
||||
public int encodedSize(QueryMessage queryMessage, ProtocolVersion version)
|
||||
{
|
||||
return QueryMessage.codec.encodedSize(queryMessage, version);
|
||||
}
|
||||
});
|
||||
|
||||
int threads = 10;
|
||||
int perThread = 30;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threads + 10);
|
||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
CountDownLatch signal = new CountDownLatch(1);
|
||||
|
||||
AtomicBoolean measure = new AtomicBoolean(false);
|
||||
DescriptiveStatistics stats = new DescriptiveStatistics();
|
||||
Lock lock = new ReentrantLock();
|
||||
for (int t = 0; t < threads; t++)
|
||||
{
|
||||
executor.execute(() -> {
|
||||
try (Cluster driver = builder.build();
|
||||
Session session = driver.connect())
|
||||
{
|
||||
while (!executor.isShutdown() && error.get() == null)
|
||||
{
|
||||
Map<Integer, ResultSetFuture> futures = new HashMap<>();
|
||||
|
||||
for (int j = 0; j < perThread; j++)
|
||||
{
|
||||
long startNanos = System.nanoTime();
|
||||
ResultSetFuture future = session.executeAsync(request);
|
||||
future.addListener(() -> {
|
||||
long diff = System.nanoTime() - startNanos;
|
||||
if (measure.get())
|
||||
{
|
||||
lock.lock();
|
||||
try
|
||||
{
|
||||
stats.addValue(TimeUnit.MICROSECONDS.toMillis(diff));
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}, executor);
|
||||
futures.put(j, future);
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, ResultSetFuture> e : futures.entrySet())
|
||||
{
|
||||
Assert.assertEquals(response.result.size(),
|
||||
e.getValue().get().all().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
error.set(e);
|
||||
signal.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Assert.assertFalse(signal.await(30, TimeUnit.SECONDS));
|
||||
measure.set(true);
|
||||
Assert.assertFalse(signal.await(60, TimeUnit.SECONDS));
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
|
||||
System.out.println("requestSize = " + requestSize);
|
||||
System.out.println("responseSize = " + responseSize);
|
||||
System.out.println("Mean: " + stats.getMean());
|
||||
System.out.println("Variance: " + stats.getVariance());
|
||||
System.out.println("Median: " + stats.getPercentile(0.5));
|
||||
System.out.println("90p: " + stats.getPercentile(0.90));
|
||||
System.out.println("95p: " + stats.getPercentile(0.95));
|
||||
System.out.println("99p: " + stats.getPercentile(0.99));
|
||||
}
|
||||
}
|
||||
// TODO: test disconnecting and reconnecting constantly
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.apache.cassandra.auth.AllowAllAuthenticator;
|
||||
import org.apache.cassandra.auth.AllowAllAuthorizer;
|
||||
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.metrics.ClientMetrics;
|
||||
import org.apache.cassandra.service.NativeTransportService;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.messages.QueryMessage;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
import org.apache.cassandra.utils.AssertUtil;
|
||||
|
||||
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.SizeCaps;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.generateQueryMessage;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.generateRows;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class SimpleClientBurnTest
|
||||
{
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CQLConnectionTest.class);
|
||||
|
||||
private InetAddress address;
|
||||
private int port;
|
||||
|
||||
@Before
|
||||
public void setup()
|
||||
{
|
||||
DatabaseDescriptor.toolInitialization();
|
||||
DatabaseDescriptor.setAuthenticator(new AllowAllAuthenticator());
|
||||
DatabaseDescriptor.setAuthorizer(new AllowAllAuthorizer());
|
||||
DatabaseDescriptor.setNetworkAuthorizer(new AllowAllNetworkAuthorizer());
|
||||
long seed = new SecureRandom().nextLong();
|
||||
logger.info("seed: {}", seed);
|
||||
address = InetAddress.getLoopbackAddress();
|
||||
try
|
||||
{
|
||||
try (ServerSocket serverSocket = new ServerSocket(0))
|
||||
{
|
||||
port = serverSocket.getLocalPort();
|
||||
}
|
||||
Thread.sleep(250);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() throws Throwable
|
||||
{
|
||||
SizeCaps smallMessageCap = new SizeCaps(5, 10, 5, 5);
|
||||
SizeCaps largeMessageCap = new SizeCaps(1000, 2000, 5, 150);
|
||||
int largeMessageFrequency = 1000;
|
||||
|
||||
CQLConnectionTest.AllocationObserver allocationObserver = new CQLConnectionTest.AllocationObserver();
|
||||
PipelineConfigurator configurator = new PipelineConfigurator(NativeTransportService.useEpoll(), false, false, UNENCRYPTED)
|
||||
{
|
||||
protected ClientResourceLimits.ResourceProvider resourceProvider(ClientResourceLimits.Allocator allocator)
|
||||
{
|
||||
return BurnTestUtil.observableResourceProvider(allocationObserver).apply(allocator);
|
||||
}
|
||||
};
|
||||
|
||||
Server server = new Server.Builder().withHost(address)
|
||||
.withPort(port)
|
||||
.withPipelineConfigurator(configurator)
|
||||
.build();
|
||||
ClientMetrics.instance.init(Collections.singleton(server));
|
||||
server.start();
|
||||
|
||||
Message.Type.QUERY.unsafeSetCodec(new Message.Codec<QueryMessage>()
|
||||
{
|
||||
public QueryMessage decode(ByteBuf body, ProtocolVersion version)
|
||||
{
|
||||
QueryMessage queryMessage = QueryMessage.codec.decode(body, version);
|
||||
return new QueryMessage(queryMessage.query, queryMessage.options)
|
||||
{
|
||||
protected Message.Response execute(QueryState state, long queryStartNanoTime, boolean traceRequest)
|
||||
{
|
||||
int idx = Integer.parseInt(queryMessage.query);
|
||||
SizeCaps caps = idx % largeMessageFrequency == 0 ? largeMessageCap : smallMessageCap;
|
||||
return generateRows(idx, caps);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void encode(QueryMessage queryMessage, ByteBuf dest, ProtocolVersion version)
|
||||
{
|
||||
QueryMessage.codec.encode(queryMessage, dest, version);
|
||||
}
|
||||
|
||||
public int encodedSize(QueryMessage queryMessage, ProtocolVersion version)
|
||||
{
|
||||
return QueryMessage.codec.encodedSize(queryMessage, version);
|
||||
}
|
||||
});
|
||||
|
||||
List<AssertUtil.ThrowingSupplier<SimpleClient>> suppliers =
|
||||
Arrays.asList(
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V5, true,
|
||||
new EncryptionOptions())
|
||||
.connect(false),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V4, false,
|
||||
new EncryptionOptions())
|
||||
.connect(false)
|
||||
);
|
||||
|
||||
int threads = 3;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threads);
|
||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
CountDownLatch signal = new CountDownLatch(1);
|
||||
|
||||
// TODO: exercise client -> server large messages
|
||||
for (int t = 0; t < threads; t++)
|
||||
{
|
||||
int threadId = t;
|
||||
executor.execute(() -> {
|
||||
try (SimpleClient client = suppliers.get(threadId % suppliers.size()).get())
|
||||
{
|
||||
int counter = 0;
|
||||
while (!executor.isShutdown() && error.get() == null)
|
||||
{
|
||||
if (counter % 100 == 0)
|
||||
System.out.println("idx = " + counter);
|
||||
List<Message.Request> messages = new ArrayList<>();
|
||||
for (int j = 0; j < 10; j++)
|
||||
{
|
||||
int descriptor = counter + j * 100 + threadId * 10000;
|
||||
SizeCaps caps = descriptor % largeMessageFrequency == 0 ? largeMessageCap : smallMessageCap;
|
||||
QueryMessage query = generateQueryMessage(descriptor, caps);
|
||||
messages.add(query);
|
||||
}
|
||||
|
||||
Map<Message.Request, Message.Response> responses = client.execute(messages);
|
||||
for (Map.Entry<Message.Request, Message.Response> entry : responses.entrySet())
|
||||
{
|
||||
int idx = Integer.parseInt(((QueryMessage) entry.getKey()).query);
|
||||
SizeCaps caps = idx % largeMessageFrequency == 0 ? largeMessageCap : smallMessageCap;
|
||||
ResultMessage.Rows actual = ((ResultMessage.Rows) entry.getValue());
|
||||
|
||||
ResultMessage.Rows expected = generateRows(idx, caps);
|
||||
Assert.assertEquals(expected.result.rows.size(), actual.result.rows.size());
|
||||
for (int i = 0; i < expected.result.rows.size(); i++)
|
||||
{
|
||||
List<ByteBuffer> expectedRow = expected.result.rows.get(i);
|
||||
List<ByteBuffer> actualRow = actual.result.rows.get(i);
|
||||
Assert.assertEquals(expectedRow.size(), actualRow.size());
|
||||
for (int col = 0; col < expectedRow.size(); col++)
|
||||
Assert.assertEquals(expectedRow.get(col), actualRow.get(col));
|
||||
}
|
||||
}
|
||||
counter++;
|
||||
System.gc(); // try to trigger leak detector
|
||||
}
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
error.set(e);
|
||||
signal.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Assert.assertFalse(signal.await(120, TimeUnit.SECONDS));
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(allocationObserver.endpointAllocationTotal()).isEqualTo(allocationObserver.endpointReleaseTotal());
|
||||
assertThat(allocationObserver.globalAllocationTotal()).isEqualTo(allocationObserver.globalReleaseTotal());
|
||||
|
||||
server.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.apache.commons.math.stat.descriptive.DescriptiveStatistics;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.apache.cassandra.auth.AllowAllAuthenticator;
|
||||
import org.apache.cassandra.auth.AllowAllAuthorizer;
|
||||
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.metrics.ClientMetrics;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.messages.QueryMessage;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
import org.apache.cassandra.utils.AssertUtil;
|
||||
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.SizeCaps;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.generateQueryMessage;
|
||||
import static org.apache.cassandra.transport.BurnTestUtil.generateRows;
|
||||
|
||||
public class SimpleClientPerfTest
|
||||
{
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CQLConnectionTest.class);
|
||||
|
||||
private InetAddress address;
|
||||
private int port;
|
||||
|
||||
@Before
|
||||
public void setup()
|
||||
{
|
||||
DatabaseDescriptor.toolInitialization();
|
||||
DatabaseDescriptor.setAuthenticator(new AllowAllAuthenticator());
|
||||
DatabaseDescriptor.setAuthorizer(new AllowAllAuthorizer());
|
||||
DatabaseDescriptor.setNetworkAuthorizer(new AllowAllNetworkAuthorizer());
|
||||
address = InetAddress.getLoopbackAddress();
|
||||
try
|
||||
{
|
||||
try (ServerSocket serverSocket = new ServerSocket(0))
|
||||
{
|
||||
port = serverSocket.getLocalPort();
|
||||
}
|
||||
Thread.sleep(250);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureSmallV5() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(10, 20, 5, 10),
|
||||
new SizeCaps(10, 20, 5, 10),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V5, true,
|
||||
new EncryptionOptions())
|
||||
.connect(false));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void measureSmallV4() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(10, 20, 5, 10),
|
||||
new SizeCaps(10, 20, 5, 10),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V4, false,
|
||||
new EncryptionOptions())
|
||||
.connect(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureSmallV5WithCompression() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(10, 20, 5, 10),
|
||||
new SizeCaps(10, 20, 5, 10),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V5, true,
|
||||
new EncryptionOptions())
|
||||
.connect(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureSmallV4WithCompression() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(10, 20, 5, 10),
|
||||
new SizeCaps(10, 20, 5, 10),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V4, false,
|
||||
new EncryptionOptions())
|
||||
.connect(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureLargeV5() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(1000, 2000, 5, 150),
|
||||
new SizeCaps(1000, 2000, 5, 150),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V5, true,
|
||||
new EncryptionOptions())
|
||||
.connect(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureLargeV4() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(1000, 2000, 5, 150),
|
||||
new SizeCaps(1000, 2000, 5, 150),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V4, false,
|
||||
new EncryptionOptions())
|
||||
.connect(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureLargeV5WithCompression() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(1000, 2000, 5, 150),
|
||||
new SizeCaps(1000, 2000, 5, 150),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V5, true,
|
||||
new EncryptionOptions())
|
||||
.connect(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void measureLargeV4WithCompression() throws Throwable
|
||||
{
|
||||
perfTest(new SizeCaps(1000, 2000, 5, 150),
|
||||
new SizeCaps(1000, 2000, 5, 150),
|
||||
() -> new SimpleClient(address.getHostAddress(),
|
||||
port, ProtocolVersion.V4, false,
|
||||
new EncryptionOptions())
|
||||
.connect(true));
|
||||
}
|
||||
|
||||
public void perfTest(SizeCaps requestCaps, SizeCaps responseCaps, AssertUtil.ThrowingSupplier<SimpleClient> clientSupplier) throws Throwable
|
||||
{
|
||||
ResultMessage.Rows response = generateRows(0, responseCaps);
|
||||
QueryMessage requestMessage = generateQueryMessage(0, requestCaps);
|
||||
Envelope message = requestMessage.encode(ProtocolVersion.V4);
|
||||
int requestSize = message.body.readableBytes();
|
||||
message.release();
|
||||
message = response.encode(ProtocolVersion.V4);
|
||||
int responseSize = message.body.readableBytes();
|
||||
message.release();
|
||||
|
||||
Server server = new Server.Builder().withHost(address)
|
||||
.withPort(port)
|
||||
.build();
|
||||
|
||||
ClientMetrics.instance.init(Collections.singleton(server));
|
||||
server.start();
|
||||
|
||||
Message.Type.QUERY.unsafeSetCodec(new Message.Codec<QueryMessage>()
|
||||
{
|
||||
public QueryMessage decode(ByteBuf body, ProtocolVersion version)
|
||||
{
|
||||
QueryMessage queryMessage = QueryMessage.codec.decode(body, version);
|
||||
return new QueryMessage(queryMessage.query, queryMessage.options)
|
||||
{
|
||||
protected Message.Response execute(QueryState state, long queryStartNanoTime, boolean traceRequest)
|
||||
{
|
||||
int idx = Integer.parseInt(queryMessage.query); // unused
|
||||
return generateRows(idx, responseCaps);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void encode(QueryMessage queryMessage, ByteBuf dest, ProtocolVersion version)
|
||||
{
|
||||
QueryMessage.codec.encode(queryMessage, dest, version);
|
||||
}
|
||||
|
||||
public int encodedSize(QueryMessage queryMessage, ProtocolVersion version)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
int threads = 1;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threads);
|
||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
CountDownLatch signal = new CountDownLatch(1);
|
||||
|
||||
AtomicBoolean measure = new AtomicBoolean(false);
|
||||
DescriptiveStatistics stats = new DescriptiveStatistics();
|
||||
Lock lock = new ReentrantLock();
|
||||
|
||||
// TODO: exercise client -> server large messages
|
||||
for (int t = 0; t < threads; t++)
|
||||
{
|
||||
executor.execute(() -> {
|
||||
try (SimpleClient client = clientSupplier.get())
|
||||
{
|
||||
while (!executor.isShutdown() && error.get() == null)
|
||||
{
|
||||
List<Message.Request> messages = new ArrayList<>();
|
||||
for (int j = 0; j < 1; j++)
|
||||
messages.add(requestMessage);
|
||||
|
||||
if (measure.get())
|
||||
{
|
||||
long nanoStart = System.nanoTime();
|
||||
client.execute(messages);
|
||||
long diff = System.nanoTime() - nanoStart;
|
||||
|
||||
lock.lock();
|
||||
try
|
||||
{
|
||||
stats.addValue(TimeUnit.MICROSECONDS.toMillis(diff));
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
else
|
||||
client.execute(messages); // warm-up
|
||||
}
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
error.set(e);
|
||||
signal.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Assert.assertFalse(signal.await(30, TimeUnit.SECONDS));
|
||||
measure.set(true);
|
||||
Assert.assertFalse(signal.await(60, TimeUnit.SECONDS));
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
|
||||
System.out.println("requestSize = " + requestSize);
|
||||
System.out.println("responseSize = " + responseSize);
|
||||
System.out.println("Mean: " + stats.getMean());
|
||||
System.out.println("Variance: " + stats.getVariance());
|
||||
System.out.println("Median: " + stats.getPercentile(0.5));
|
||||
System.out.println("90p: " + stats.getPercentile(0.90));
|
||||
System.out.println("95p: " + stats.getPercentile(0.95));
|
||||
System.out.println("99p: " + stats.getPercentile(0.99));
|
||||
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10275,7 +10275,7 @@ org.apache.cassandra.metrics:type=ClientRequest,scope=Write-TWO,name=Unavailable
|
|||
- name: objectName
|
||||
parameters: []
|
||||
returnType: javax.management.ObjectName
|
||||
org.apache.cassandra.metrics:type=ClientRequestSize,name=BytesRecievedPerFrame:
|
||||
org.apache.cassandra.metrics:type=ClientMessageSize,name=BytesReceivedPerRequest:
|
||||
attributes:
|
||||
- {access: read-only, name: 50thPercentile, type: double}
|
||||
- {access: read-only, name: 75thPercentile, type: double}
|
||||
|
|
@ -10296,7 +10296,7 @@ org.apache.cassandra.metrics:type=ClientRequestSize,name=BytesRecievedPerFrame:
|
|||
- name: values
|
||||
parameters: []
|
||||
returnType: long[]
|
||||
org.apache.cassandra.metrics:type=ClientRequestSize,name=BytesTransmittedPerFrame:
|
||||
org.apache.cassandra.metrics:type=ClientMessageSize,name=BytesSentPerResponse:
|
||||
attributes:
|
||||
- {access: read-only, name: 50thPercentile, type: double}
|
||||
- {access: read-only, name: 75thPercentile, type: double}
|
||||
|
|
@ -10317,14 +10317,14 @@ org.apache.cassandra.metrics:type=ClientRequestSize,name=BytesTransmittedPerFram
|
|||
- name: values
|
||||
parameters: []
|
||||
returnType: long[]
|
||||
org.apache.cassandra.metrics:type=ClientRequestSize,name=IncomingBytes:
|
||||
org.apache.cassandra.metrics:type=ClientMessageSize,name=BytesReceived:
|
||||
attributes:
|
||||
- {access: read-only, name: Count, type: long}
|
||||
operations:
|
||||
- name: objectName
|
||||
parameters: []
|
||||
returnType: javax.management.ObjectName
|
||||
org.apache.cassandra.metrics:type=ClientRequestSize,name=OutgoingBytes:
|
||||
org.apache.cassandra.metrics:type=ClientMessageSize,name=BytesSent:
|
||||
attributes:
|
||||
- {access: read-only, name: Count, type: long}
|
||||
operations:
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.test.microbench;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.transport.Frame;
|
||||
import org.apache.cassandra.transport.frame.checksum.ChecksummingTransformer;
|
||||
import org.apache.cassandra.transport.frame.compress.LZ4Compressor;
|
||||
import org.apache.cassandra.utils.ChecksumType;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.Fork;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
|
||||
@Warmup(iterations = 4, time = 1)
|
||||
@Measurement(iterations = 8, time = 2)
|
||||
@Fork(value = 2)
|
||||
@State(Scope.Benchmark)
|
||||
public class ChecksummingTransformerLz4
|
||||
{
|
||||
private static final EnumSet<Frame.Header.Flag> FLAGS = EnumSet.of(Frame.Header.Flag.COMPRESSED, Frame.Header.Flag.CHECKSUMMED);
|
||||
|
||||
static {
|
||||
DatabaseDescriptor.clientInitialization();
|
||||
}
|
||||
|
||||
private final ChecksummingTransformer transformer = ChecksummingTransformer.getTransformer(ChecksumType.CRC32, LZ4Compressor.INSTANCE);
|
||||
private ByteBuf smallEnglishASCIICompressed;
|
||||
private ByteBuf smallEnglishUtf8Compressed;
|
||||
private ByteBuf largeBlobCompressed;
|
||||
|
||||
@Setup
|
||||
public void setup() throws IOException
|
||||
{
|
||||
byte[] smallEnglishASCII = "this is small".getBytes(StandardCharsets.US_ASCII);
|
||||
this.smallEnglishASCIICompressed = transformer.transformOutbound(Unpooled.wrappedBuffer(smallEnglishASCII));
|
||||
byte[] smallEnglishUtf8 = "this is small".getBytes(StandardCharsets.UTF_8);
|
||||
this.smallEnglishUtf8Compressed = transformer.transformOutbound(Unpooled.wrappedBuffer(smallEnglishUtf8));
|
||||
|
||||
String failureHex;
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("test/data/CASSANDRA-15313/lz4-jvm-crash-failure.txt"), StandardCharsets.UTF_8))) {
|
||||
failureHex = reader.readLine().trim();
|
||||
}
|
||||
byte[] failure = ByteBufUtil.decodeHexDump(failureHex);
|
||||
this.largeBlobCompressed = transformer.transformOutbound(Unpooled.wrappedBuffer(failure));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public ByteBuf decompresSsmallEnglishASCII() {
|
||||
smallEnglishASCIICompressed.readerIndex(0);
|
||||
return transformer.transformInbound(smallEnglishASCIICompressed, FLAGS);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public ByteBuf decompresSsmallEnglishUtf8() {
|
||||
smallEnglishUtf8Compressed.readerIndex(0);
|
||||
return transformer.transformInbound(smallEnglishUtf8Compressed, FLAGS);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public ByteBuf decompresLargeBlob() {
|
||||
largeBlobCompressed.readerIndex(0);
|
||||
return transformer.transformInbound(largeBlobCompressed, FLAGS);
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ import java.util.concurrent.CountDownLatch;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -501,17 +502,47 @@ public abstract class CQLTester
|
|||
|
||||
// lazy initialization for all tests that require Java Driver
|
||||
protected static void requireNetwork() throws ConfigurationException
|
||||
{
|
||||
requireNetwork(server -> {});
|
||||
}
|
||||
|
||||
// lazy initialization for all tests that require Java Driver
|
||||
protected static void requireNetwork(Consumer<Server.Builder> decorator) throws ConfigurationException
|
||||
{
|
||||
if (server != null)
|
||||
return;
|
||||
|
||||
SystemKeyspace.finishStartup();
|
||||
VirtualKeyspaceRegistry.instance.register(VirtualSchemaKeyspace.instance);
|
||||
|
||||
StorageService.instance.initServer();
|
||||
SchemaLoader.startGossiper();
|
||||
initializeNetwork(decorator);
|
||||
}
|
||||
|
||||
server = new Server.Builder().withHost(nativeAddr).withPort(nativePort).build();
|
||||
protected static void reinitializeNetwork()
|
||||
{
|
||||
if (server != null && server.isRunning())
|
||||
{
|
||||
server.stop();
|
||||
server = null;
|
||||
}
|
||||
List<CloseFuture> futures = new ArrayList<>();
|
||||
for (Cluster cluster : clusters.values())
|
||||
futures.add(cluster.closeAsync());
|
||||
for (Session session : sessions.values())
|
||||
futures.add(session.closeAsync());
|
||||
FBUtilities.waitOnFutures(futures);
|
||||
clusters.clear();
|
||||
sessions.clear();
|
||||
|
||||
initializeNetwork(server -> {});
|
||||
}
|
||||
|
||||
private static void initializeNetwork(Consumer<Server.Builder> decorator)
|
||||
{
|
||||
Server.Builder serverBuilder = new Server.Builder().withHost(nativeAddr).withPort(nativePort);
|
||||
decorator.accept(serverBuilder);
|
||||
server = serverBuilder.build();
|
||||
ClientMetrics.instance.init(Collections.singleton(server));
|
||||
server.start();
|
||||
|
||||
|
|
@ -1043,14 +1074,10 @@ public abstract class CQLTester
|
|||
return sessions.get(protocolVersion);
|
||||
}
|
||||
|
||||
protected SimpleClient newSimpleClient(ProtocolVersion version, boolean compression, boolean checksums, boolean isOverloadedException) throws IOException
|
||||
protected SimpleClient newSimpleClient(ProtocolVersion version) throws IOException
|
||||
{
|
||||
return new SimpleClient(nativeAddr.getHostAddress(), nativePort, version, version.isBeta(), new EncryptionOptions().applyConfig()).connect(compression, checksums, isOverloadedException);
|
||||
}
|
||||
|
||||
protected SimpleClient newSimpleClient(ProtocolVersion version, boolean compression, boolean checksums) throws IOException
|
||||
{
|
||||
return newSimpleClient(version, compression, checksums, false);
|
||||
return new SimpleClient(nativeAddr.getHostAddress(), nativePort, version, version.isBeta(), new EncryptionOptions().applyConfig())
|
||||
.connect(false, false);
|
||||
}
|
||||
|
||||
protected String formatQuery(String query)
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ public class PreparedStatementsTest extends CQLTester
|
|||
createTable("CREATE TABLE %s (pk int, v1 int, v2 int, PRIMARY KEY (pk))");
|
||||
execute("INSERT INTO %s (pk, v1, v2) VALUES (1,1,1)");
|
||||
|
||||
try (SimpleClient simpleClient = newSimpleClient(ProtocolVersion.BETA.orElse(ProtocolVersion.CURRENT), false, false))
|
||||
try (SimpleClient simpleClient = newSimpleClient(ProtocolVersion.BETA.orElse(ProtocolVersion.CURRENT)))
|
||||
{
|
||||
ResultMessage.Prepared prepUpdate = simpleClient.prepare(String.format("UPDATE %s.%s SET v1 = ?, v2 = ? WHERE pk = 1 IF v1 = ?",
|
||||
keyspace(), currentTable()));
|
||||
|
|
|
|||
|
|
@ -18,18 +18,22 @@
|
|||
|
||||
package org.apache.cassandra.metrics;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import com.codahale.metrics.Counter;
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Snapshot;
|
||||
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
import org.apache.cassandra.metrics.ClearableHistogram;
|
||||
import org.apache.cassandra.metrics.ClientRequestSizeMetrics;
|
||||
import org.apache.cassandra.metrics.DecayingEstimatedHistogramReservoir.EstimatedHistogramReservoirSnapshot;
|
||||
import org.apache.cassandra.metrics.DecayingEstimatedHistogramReservoir.Range;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
|
@ -37,8 +41,21 @@ import static org.junit.Assert.assertEquals;
|
|||
/**
|
||||
* Ensures we properly account for metrics tracked in the native protocol
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class ClientRequestSizeMetricsTest extends CQLTester
|
||||
{
|
||||
|
||||
@Parameterized.Parameter
|
||||
public ProtocolVersion version;
|
||||
|
||||
@Parameterized.Parameters()
|
||||
public static Collection<Object[]> versions()
|
||||
{
|
||||
return ProtocolVersion.SUPPORTED.stream()
|
||||
.map(v -> new Object[]{v})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp()
|
||||
{
|
||||
|
|
@ -52,10 +69,10 @@ public class ClientRequestSizeMetricsTest extends CQLTester
|
|||
// the event sent upon schema updates
|
||||
clearMetrics();
|
||||
|
||||
executeNet("SELECT * from system.peers");
|
||||
executeNet(version, "SELECT * from system.peers");
|
||||
|
||||
long requestLength = ClientRequestSizeMetrics.totalBytesRead.getCount();
|
||||
long responseLength = ClientRequestSizeMetrics.totalBytesWritten.getCount();
|
||||
long requestLength = ClientMessageSizeMetrics.bytesReceived.getCount();
|
||||
long responseLength = ClientMessageSizeMetrics.bytesSent.getCount();
|
||||
|
||||
assertThat(requestLength).isGreaterThan(0);
|
||||
assertThat(responseLength).isGreaterThan(0);
|
||||
|
|
@ -63,7 +80,7 @@ public class ClientRequestSizeMetricsTest extends CQLTester
|
|||
checkMetrics(1, requestLength, responseLength);
|
||||
|
||||
// Let's fire the same request again and test that the changes are the same that previously
|
||||
executeNet("SELECT * from system.peers");
|
||||
executeNet(version, "SELECT * from system.peers");
|
||||
|
||||
checkMetrics(2, requestLength, responseLength);
|
||||
}
|
||||
|
|
@ -72,22 +89,22 @@ public class ClientRequestSizeMetricsTest extends CQLTester
|
|||
{
|
||||
Snapshot snapshot;
|
||||
long expectedTotalBytesRead = numberOfRequests * requestLength;
|
||||
assertEquals(expectedTotalBytesRead, ClientRequestSizeMetrics.totalBytesRead.getCount());
|
||||
assertEquals(expectedTotalBytesRead, ClientMessageSizeMetrics.bytesReceived.getCount());
|
||||
|
||||
long expectedTotalBytesWritten = numberOfRequests * responseLength;
|
||||
assertEquals(expectedTotalBytesWritten, ClientRequestSizeMetrics.totalBytesWritten.getCount());
|
||||
assertEquals(expectedTotalBytesWritten, ClientMessageSizeMetrics.bytesSent.getCount());
|
||||
|
||||
// The request fit in one single frame so we know the new number of received frames
|
||||
assertEquals(numberOfRequests, ClientRequestSizeMetrics.bytesReceivedPerFrame.getCount());
|
||||
assertEquals(numberOfRequests, ClientMessageSizeMetrics.bytesReceivedPerRequest.getCount());
|
||||
|
||||
snapshot = ClientRequestSizeMetrics.bytesReceivedPerFrame.getSnapshot();
|
||||
snapshot = ClientMessageSizeMetrics.bytesReceivedPerRequest.getSnapshot();
|
||||
assertMin(snapshot, requestLength);
|
||||
assertMax(snapshot, requestLength);
|
||||
|
||||
// The response fit in one single frame so we know the new number of received frames
|
||||
assertEquals(numberOfRequests, ClientRequestSizeMetrics.bytesTransmittedPerFrame.getCount());
|
||||
assertEquals(numberOfRequests, ClientMessageSizeMetrics.bytesSentPerResponse.getCount());
|
||||
|
||||
snapshot = ClientRequestSizeMetrics.bytesTransmittedPerFrame.getSnapshot();
|
||||
snapshot = ClientMessageSizeMetrics.bytesSentPerResponse.getSnapshot();
|
||||
assertMin(snapshot, responseLength);
|
||||
assertMax(snapshot, responseLength);
|
||||
}
|
||||
|
|
@ -106,10 +123,10 @@ public class ClientRequestSizeMetricsTest extends CQLTester
|
|||
|
||||
private void clearMetrics()
|
||||
{
|
||||
clearCounter(ClientRequestSizeMetrics.totalBytesRead);
|
||||
clearCounter(ClientRequestSizeMetrics.totalBytesWritten);
|
||||
clearHistogram(ClientRequestSizeMetrics.bytesReceivedPerFrame);
|
||||
clearHistogram(ClientRequestSizeMetrics.bytesTransmittedPerFrame);
|
||||
clearCounter(ClientMessageSizeMetrics.bytesReceived);
|
||||
clearCounter(ClientMessageSizeMetrics.bytesSent);
|
||||
clearHistogram(ClientMessageSizeMetrics.bytesReceivedPerRequest);
|
||||
clearHistogram(ClientMessageSizeMetrics.bytesSentPerResponse);
|
||||
}
|
||||
|
||||
private void clearCounter(Counter counter)
|
||||
|
|
|
|||
|
|
@ -455,9 +455,9 @@ public class FramingTest
|
|||
return new SequenceOfFrames(messages, cumulativeLength, frames);
|
||||
}
|
||||
|
||||
private static byte[] randomishBytes(Random random, int minLength, int maxLength)
|
||||
public static byte[] randomishBytes(Random random, int minLength, int maxLength)
|
||||
{
|
||||
byte[] bytes = new byte[minLength + random.nextInt(maxLength - minLength)];
|
||||
byte[] bytes = new byte[minLength + random.nextInt(Math.max(1, maxLength - minLength))];
|
||||
int runLength = 1 + random.nextInt(255);
|
||||
for (int i = 0 ; i < bytes.length ; i += runLength)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class ClientWarningsTest extends CQLTester
|
|||
|
||||
try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V4))
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
|
||||
QueryMessage query = new QueryMessage(createBatchStatement2(1), QueryOptions.DEFAULT);
|
||||
Message.Response resp = client.execute(query);
|
||||
|
|
@ -70,7 +70,7 @@ public class ClientWarningsTest extends CQLTester
|
|||
|
||||
try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V4))
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
|
||||
QueryMessage query = new QueryMessage(createBatchStatement2(DatabaseDescriptor.getBatchSizeWarnThreshold() / 2 + 1), QueryOptions.DEFAULT);
|
||||
Message.Response resp = client.execute(query);
|
||||
|
|
@ -90,7 +90,7 @@ public class ClientWarningsTest extends CQLTester
|
|||
|
||||
try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V4))
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
|
|
@ -130,7 +130,7 @@ public class ClientWarningsTest extends CQLTester
|
|||
|
||||
try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V3))
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
|
||||
QueryMessage query = new QueryMessage(createBatchStatement(DatabaseDescriptor.getBatchSizeWarnThreshold()), QueryOptions.DEFAULT);
|
||||
Message.Response resp = client.execute(query);
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ public class ProtocolBetaVersionTest extends CQLTester
|
|||
|
||||
try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, betaVersion, true, new EncryptionOptions()))
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
QueryMessage query = new QueryMessage(String.format("INSERT INTO %s.%s (pk, v) VALUES (%s, %s)",
|
||||
|
|
@ -105,7 +105,7 @@ public class ProtocolBetaVersionTest extends CQLTester
|
|||
assertTrue(betaVersion.isBeta()); // change to another beta version or remove test if no beta version
|
||||
try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, betaVersion, false, new EncryptionOptions()))
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
fail("Exception should have been thrown");
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,919 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.LongConsumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import org.apache.cassandra.auth.AllowAllAuthenticator;
|
||||
import org.apache.cassandra.auth.AllowAllAuthorizer;
|
||||
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
|
||||
import org.apache.cassandra.concurrent.NamedThreadFactory;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.exceptions.ExceptionCode;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.net.*;
|
||||
import org.apache.cassandra.net.proxy.InboundProxyHandler;
|
||||
import org.apache.cassandra.service.NativeTransportService;
|
||||
import org.apache.cassandra.transport.CQLMessageHandler.MessageConsumer;
|
||||
import org.apache.cassandra.transport.messages.*;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.concurrent.SimpleCondition;
|
||||
|
||||
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
|
||||
import static org.apache.cassandra.net.FramingTest.randomishBytes;
|
||||
import static org.apache.cassandra.transport.Flusher.MAX_FRAMED_PAYLOAD_SIZE;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class CQLConnectionTest
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(CQLConnectionTest.class);
|
||||
|
||||
private Random random;
|
||||
private InetAddress address;
|
||||
private int port;
|
||||
private BufferPoolAllocator alloc;
|
||||
|
||||
@Before
|
||||
public void setup()
|
||||
{
|
||||
DatabaseDescriptor.toolInitialization();
|
||||
DatabaseDescriptor.setAuthenticator(new AllowAllAuthenticator());
|
||||
DatabaseDescriptor.setAuthorizer(new AllowAllAuthorizer());
|
||||
DatabaseDescriptor.setNetworkAuthorizer(new AllowAllNetworkAuthorizer());
|
||||
long seed = new SecureRandom().nextLong();
|
||||
logger.info("seed: {}", seed);
|
||||
random = new Random(seed);
|
||||
address = InetAddress.getLoopbackAddress();
|
||||
try
|
||||
{
|
||||
try (ServerSocket serverSocket = new ServerSocket(0))
|
||||
{
|
||||
port = serverSocket.getLocalPort();
|
||||
}
|
||||
Thread.sleep(250);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
alloc = GlobalBufferPoolAllocator.instance;
|
||||
// set connection-local queue size to 0 so that all capacity is allocated from reserves
|
||||
DatabaseDescriptor.setNativeTransportReceiveQueueCapacityInBytes(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleErrorDuringNegotiation() throws Throwable
|
||||
{
|
||||
int messageCount = 0;
|
||||
Codec codec = Codec.crc(alloc);
|
||||
AllocationObserver observer = new AllocationObserver();
|
||||
InboundProxyHandler.Controller controller = new InboundProxyHandler.Controller();
|
||||
// Force protocol version to an unsupported version
|
||||
controller.withPayloadTransform(msg -> {
|
||||
ByteBuf bb = (ByteBuf)msg;
|
||||
bb.setByte(0, 99 & Envelope.PROTOCOL_VERSION_MASK);
|
||||
return msg;
|
||||
});
|
||||
|
||||
ServerConfigurator configurator = ServerConfigurator.builder()
|
||||
.withAllocationObserver(observer)
|
||||
.withProxyController(controller)
|
||||
.build();
|
||||
Server server = server(configurator);
|
||||
Client client = new Client(codec, messageCount);
|
||||
server.start();
|
||||
client.connect(address, port);
|
||||
assertFalse(client.isConnected());
|
||||
assertThat(client.getConnectionError())
|
||||
.isNotNull()
|
||||
.matches(message ->
|
||||
message.error.getMessage()
|
||||
.equals("Invalid or unsupported protocol version (99); " +
|
||||
"supported versions are (3/v3, 4/v4, 5/v5-beta)"));
|
||||
server.stop();
|
||||
|
||||
// the failure happens before any capacity is allocated
|
||||
observer.verifier().accept(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleCorruptionAfterNegotiation() throws Throwable
|
||||
{
|
||||
// A corrupt messaging frame should terminate the connection as clients
|
||||
// generally don't track which stream IDs are present in the frame, and the
|
||||
// server has no way to signal which streams are affected.
|
||||
// Before closing, the server should send an ErrorMessage to inform the
|
||||
// client of the corrupt message.
|
||||
int messageCount = 10;
|
||||
Codec codec = Codec.crc(alloc);
|
||||
AllocationObserver observer = new AllocationObserver();
|
||||
InboundProxyHandler.Controller controller = new InboundProxyHandler.Controller();
|
||||
|
||||
ServerConfigurator configurator = ServerConfigurator.builder()
|
||||
.withAllocationObserver(observer)
|
||||
.withProxyController(controller)
|
||||
.build();
|
||||
Server server = server(configurator);
|
||||
Client client = new Client(codec, messageCount);
|
||||
server.start();
|
||||
client.connect(address, port);
|
||||
assertTrue(client.isConnected());
|
||||
|
||||
// Only install the transform after protocol negotiation is complete
|
||||
controller.withPayloadTransform(msg -> {
|
||||
// Corrupt frame
|
||||
ByteBuf bb = (ByteBuf) msg;
|
||||
bb.setByte(bb.readableBytes() / 2, 0xffff);
|
||||
return msg;
|
||||
});
|
||||
|
||||
for (int i=0; i < messageCount; i++)
|
||||
client.send(randomEnvelope(i, Message.Type.OPTIONS));
|
||||
|
||||
client.awaitResponses();
|
||||
// Client has disconnected
|
||||
assertFalse(client.isConnected());
|
||||
// But before it did, it sent an error response
|
||||
Envelope receieved = client.inboundMessages.poll();
|
||||
assertNotNull(receieved);
|
||||
Message.Response response = Message.responseDecoder().decode(client.channel, receieved);
|
||||
assertEquals(Message.Type.ERROR, response.type);
|
||||
assertTrue(((ErrorMessage)response).error.getMessage().contains("unrecoverable CRC mismatch detected in frame body"));
|
||||
|
||||
// the failure happens before any capacity is allocated
|
||||
observer.verifier().accept(0);
|
||||
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleCorruptionOfLargeMessage() throws Throwable
|
||||
{
|
||||
// A corrupt messaging frame should terminate the connection as clients
|
||||
// generally don't track which stream IDs are present in the frame, and the
|
||||
// server has no way to signal which streams are affected.
|
||||
// Before closing, the server should send an ErrorMessage to inform the
|
||||
// client of the corrupt message.
|
||||
// Client needs to expect multiple responses or else awaitResponses returns
|
||||
// after the error is first received and we race between handling the exception
|
||||
// caused by remote disconnection and checking the connection status.
|
||||
int messageCount = 2;
|
||||
Codec codec = Codec.crc(alloc);
|
||||
AllocationObserver observer = new AllocationObserver();
|
||||
InboundProxyHandler.Controller controller = new InboundProxyHandler.Controller();
|
||||
|
||||
ServerConfigurator configurator = ServerConfigurator.builder()
|
||||
.withAllocationObserver(observer)
|
||||
.withProxyController(controller)
|
||||
.build();
|
||||
Server server = server(configurator);
|
||||
Client client = new Client(codec, messageCount);
|
||||
server.start();
|
||||
client.connect(address, port);
|
||||
assertTrue(client.isConnected());
|
||||
|
||||
// Only install the corrupting transform after protocol negotiation is complete
|
||||
controller.withPayloadTransform(new Function<Object, Object>()
|
||||
{
|
||||
// Don't corrupt the first frame as this would fail early and bypass capacity allocation.
|
||||
// Instead, allow enough bytes to fill the first frame through untouched. Then, corrupt
|
||||
// a byte which will be in the second frame of the large message .
|
||||
int seenBytes = 0;
|
||||
int corruptedByte = 0;
|
||||
public Object apply(Object o)
|
||||
{
|
||||
// If we've already injected some corruption, pass through
|
||||
if (corruptedByte > 0)
|
||||
return o;
|
||||
|
||||
// Will the current buffer size take us into the second frame? If so, corrupt it
|
||||
ByteBuf bb = (ByteBuf)o;
|
||||
if (seenBytes + bb.readableBytes() > MAX_FRAMED_PAYLOAD_SIZE + 100)
|
||||
{
|
||||
int frameBoundary = MAX_FRAMED_PAYLOAD_SIZE - seenBytes;
|
||||
corruptedByte = bb.readerIndex() + frameBoundary + 100;
|
||||
bb.setByte(corruptedByte, 0xffff);
|
||||
}
|
||||
else
|
||||
{
|
||||
seenBytes += bb.readableBytes();
|
||||
}
|
||||
|
||||
return bb;
|
||||
}
|
||||
});
|
||||
|
||||
int totalBytes = MAX_FRAMED_PAYLOAD_SIZE * 2;
|
||||
client.send(randomEnvelope(0, Message.Type.OPTIONS, totalBytes, totalBytes));
|
||||
client.awaitResponses();
|
||||
// Client has disconnected
|
||||
assertFalse(client.isConnected());
|
||||
// But before it did, it received an error response
|
||||
Envelope received = client.inboundMessages.poll();
|
||||
assertNotNull(received);
|
||||
Message.Response response = Message.responseDecoder().decode(client.channel, received);
|
||||
assertEquals(Message.Type.ERROR, response.type);
|
||||
assertTrue(((ErrorMessage)response).error.getMessage().contains("unrecoverable CRC mismatch detected in frame"));
|
||||
// total capacity is aquired when the first frame is read
|
||||
observer.verifier().accept(totalBytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAquireAndRelease()
|
||||
{
|
||||
acquireAndRelease(10, 100, Codec.crc(alloc));
|
||||
acquireAndRelease(10, 100, Codec.lz4(alloc));
|
||||
|
||||
acquireAndRelease(100, 1000, Codec.crc(alloc));
|
||||
acquireAndRelease(100, 1000, Codec.lz4(alloc));
|
||||
|
||||
acquireAndRelease(1000, 10000, Codec.crc(alloc));
|
||||
acquireAndRelease(1000, 10000, Codec.lz4(alloc));
|
||||
}
|
||||
|
||||
private void acquireAndRelease(int minMessages, int maxMessages, Codec codec)
|
||||
{
|
||||
final int messageCount = minMessages + random.nextInt(maxMessages - minMessages);
|
||||
logger.info("Sending total of {} messages", messageCount);
|
||||
|
||||
TestConsumer consumer = new TestConsumer(new ResultMessage.Void(), codec.encoder);
|
||||
AllocationObserver observer = new AllocationObserver();
|
||||
Message.Decoder<Message.Request> decoder = new FixedDecoder();
|
||||
Predicate<Envelope.Header> responseMatcher = h -> h.type == Message.Type.RESULT;
|
||||
ServerConfigurator configurator = ServerConfigurator.builder()
|
||||
.withConsumer(consumer)
|
||||
.withAllocationObserver(observer)
|
||||
.withDecoder(decoder)
|
||||
.build();
|
||||
|
||||
runTest(configurator, codec, messageCount, responseMatcher, observer.verifier());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessageDecodingErrorEncounteredMidFrame()
|
||||
{
|
||||
messageDecodingErrorEncounteredMidFrame(10, Codec.crc(alloc));
|
||||
messageDecodingErrorEncounteredMidFrame(10, Codec.lz4(alloc));
|
||||
|
||||
messageDecodingErrorEncounteredMidFrame(100, Codec.crc(alloc));
|
||||
messageDecodingErrorEncounteredMidFrame(100, Codec.lz4(alloc));
|
||||
|
||||
messageDecodingErrorEncounteredMidFrame(1000, Codec.crc(alloc));
|
||||
messageDecodingErrorEncounteredMidFrame(1000, Codec.lz4(alloc));
|
||||
}
|
||||
|
||||
private void messageDecodingErrorEncounteredMidFrame(int messageCount, Codec codec)
|
||||
{
|
||||
final int streamWithError = messageCount / 2;
|
||||
TestConsumer consumer = new TestConsumer(new ResultMessage.Void(), codec.encoder);
|
||||
AllocationObserver observer = new AllocationObserver();
|
||||
Message.Decoder<Message.Request> decoder = new FixedDecoder()
|
||||
{
|
||||
Message.Request decode(Channel channel, Envelope source)
|
||||
{
|
||||
if (source.header.streamId != streamWithError)
|
||||
return super.decode(channel, source);
|
||||
|
||||
throw new RequestExecutionException(ExceptionCode.SYNTAX_ERROR,
|
||||
"Error decoding message " + source.header.streamId)
|
||||
{/*test exception*/};
|
||||
}
|
||||
};
|
||||
|
||||
Predicate<Envelope.Header> responseMatcher =
|
||||
h -> (h.streamId == streamWithError && h.type == Message.Type.ERROR) || h.type == Message.Type.RESULT;
|
||||
|
||||
ServerConfigurator configurator = ServerConfigurator.builder()
|
||||
.withConsumer(consumer)
|
||||
.withAllocationObserver(observer)
|
||||
.withDecoder(decoder)
|
||||
.build();
|
||||
|
||||
runTest(configurator, codec, messageCount, responseMatcher, observer.verifier());
|
||||
}
|
||||
|
||||
private void runTest(ServerConfigurator configurator,
|
||||
Codec codec,
|
||||
int messageCount,
|
||||
Predicate<Envelope.Header> responseMatcher,
|
||||
LongConsumer allocationVerifier)
|
||||
{
|
||||
Server server = server(configurator);
|
||||
Client client = new Client(codec, messageCount);
|
||||
try
|
||||
{
|
||||
server.start();
|
||||
client.connect(address, port);
|
||||
assertTrue(configurator.waitUntilReady());
|
||||
|
||||
for (int i = 0; i < messageCount; i++)
|
||||
client.send(randomEnvelope(i, Message.Type.OPTIONS));
|
||||
|
||||
long totalBytes = client.sendSize;
|
||||
|
||||
// verify that all messages went through the pipeline & our test message consumer
|
||||
client.awaitResponses();
|
||||
Envelope response;
|
||||
while ((response = client.pollResponses()) != null)
|
||||
{
|
||||
response.release();
|
||||
assertThat(response.header).matches(responseMatcher);
|
||||
}
|
||||
|
||||
// verify that we did have to acquire some resources from the global/endpoint reserves
|
||||
allocationVerifier.accept(totalBytes);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
logger.error("Unexpected error", t);
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.stop();
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private Server server(ServerConfigurator configurator)
|
||||
{
|
||||
return new Server.Builder().withHost(address)
|
||||
.withPort(port)
|
||||
.withPipelineConfigurator(configurator)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Envelope randomEnvelope(int streamId, Message.Type type)
|
||||
{
|
||||
return randomEnvelope(streamId, type, 100, 1024);
|
||||
}
|
||||
|
||||
private Envelope randomEnvelope(int streamId, Message.Type type, int minSize, int maxSize)
|
||||
{
|
||||
byte[] bytes = randomishBytes(random, minSize, maxSize);
|
||||
return Envelope.create(type,
|
||||
streamId,
|
||||
ProtocolVersion.V5,
|
||||
EnumSet.of(Envelope.Header.Flag.USE_BETA),
|
||||
Unpooled.wrappedBuffer(bytes));
|
||||
}
|
||||
|
||||
// Every CQL Envelope received will be parsed as an OptionsMessage, which is trivial to execute
|
||||
// on the server. This means we can randomise the actual content of the CQL messages to test
|
||||
// resource allocation/release (which is based purely on request size), without having to
|
||||
// worry about processing of the actual messages.
|
||||
static class FixedDecoder extends Message.Decoder<Message.Request>
|
||||
{
|
||||
Message.Request decode(Channel channel, Envelope source)
|
||||
{
|
||||
Message.Request request = new OptionsMessage();
|
||||
request.setSource(source);
|
||||
request.setStreamId(source.header.streamId);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
// A simple consumer which "serves" a static response and employs a naive flusher
|
||||
static class TestConsumer implements MessageConsumer<Message.Request>
|
||||
{
|
||||
final Message.Response fixedResponse;
|
||||
final Envelope responseTemplate;
|
||||
final FrameEncoder frameEncoder;
|
||||
SimpleClient.SimpleFlusher flusher;
|
||||
|
||||
TestConsumer(Message.Response fixedResponse, FrameEncoder frameEncoder)
|
||||
{
|
||||
this.fixedResponse = fixedResponse;
|
||||
this.responseTemplate = fixedResponse.encode(ProtocolVersion.V5);
|
||||
this.frameEncoder = frameEncoder;
|
||||
}
|
||||
|
||||
public void accept(Channel channel, Message.Request message, Dispatcher.FlushItemConverter toFlushItem)
|
||||
{
|
||||
if (flusher == null)
|
||||
flusher = new SimpleClient.SimpleFlusher(frameEncoder);
|
||||
|
||||
Flusher.FlushItem.Framed item = (Flusher.FlushItem.Framed)toFlushItem.toFlushItem(channel, message, fixedResponse);
|
||||
Envelope response = Envelope.create(responseTemplate.header.type,
|
||||
message.getStreamId(),
|
||||
ProtocolVersion.V5,
|
||||
responseTemplate.header.flags,
|
||||
responseTemplate.body.copy());
|
||||
item.release();
|
||||
flusher.enqueue(response);
|
||||
|
||||
// Schedule the proto-flusher to collate any messages to be served
|
||||
// and flush them to the outbound pipeline
|
||||
flusher.schedule(channel.pipeline().lastContext());
|
||||
}
|
||||
}
|
||||
|
||||
static class ServerConfigurator extends PipelineConfigurator
|
||||
{
|
||||
private final SimpleCondition pipelineReady = new SimpleCondition();
|
||||
private final MessageConsumer<Message.Request> consumer;
|
||||
private final AllocationObserver allocationObserver;
|
||||
private final Message.Decoder<Message.Request> decoder;
|
||||
private final InboundProxyHandler.Controller proxyController;
|
||||
|
||||
public ServerConfigurator(Builder builder)
|
||||
{
|
||||
super(NativeTransportService.useEpoll(), false, false, UNENCRYPTED);
|
||||
this.consumer = builder.consumer;
|
||||
this.decoder = builder.decoder;
|
||||
this.allocationObserver = builder.observer;
|
||||
this.proxyController = builder.proxyController;
|
||||
}
|
||||
|
||||
static Builder builder()
|
||||
{
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static class Builder
|
||||
{
|
||||
MessageConsumer<Message.Request> consumer;
|
||||
AllocationObserver observer;
|
||||
Message.Decoder<Message.Request> decoder;
|
||||
InboundProxyHandler.Controller proxyController;
|
||||
|
||||
Builder withConsumer(MessageConsumer<Message.Request> consumer)
|
||||
{
|
||||
this.consumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder withDecoder(Message.Decoder<Message.Request> decoder)
|
||||
{
|
||||
this.decoder = decoder;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder withAllocationObserver(AllocationObserver observer)
|
||||
{
|
||||
this.observer = observer;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder withProxyController(InboundProxyHandler.Controller proxyController)
|
||||
{
|
||||
this.proxyController = proxyController;
|
||||
return this;
|
||||
}
|
||||
|
||||
ServerConfigurator build()
|
||||
{
|
||||
return new ServerConfigurator(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected Message.Decoder<Message.Request> messageDecoder()
|
||||
{
|
||||
return decoder == null ? super.messageDecoder() : decoder;
|
||||
}
|
||||
|
||||
protected void onInitialPipelineReady(ChannelPipeline pipeline)
|
||||
{
|
||||
if (proxyController != null)
|
||||
{
|
||||
InboundProxyHandler proxy = new InboundProxyHandler(proxyController);
|
||||
pipeline.addFirst("PROXY", proxy);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onNegotiationComplete(ChannelPipeline pipeline)
|
||||
{
|
||||
pipelineReady.signalAll();
|
||||
}
|
||||
|
||||
private boolean waitUntilReady() throws InterruptedException
|
||||
{
|
||||
return pipelineReady.await(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
protected ClientResourceLimits.ResourceProvider resourceProvider(ClientResourceLimits.Allocator limits)
|
||||
{
|
||||
final ClientResourceLimits.ResourceProvider.Default delegate =
|
||||
new ClientResourceLimits.ResourceProvider.Default(limits);
|
||||
|
||||
if (null == allocationObserver)
|
||||
return delegate;
|
||||
|
||||
return new ClientResourceLimits.ResourceProvider()
|
||||
{
|
||||
public ResourceLimits.Limit globalLimit()
|
||||
{
|
||||
return allocationObserver.global(delegate.globalLimit());
|
||||
}
|
||||
|
||||
public AbstractMessageHandler.WaitQueue globalWaitQueue()
|
||||
{
|
||||
return delegate.globalWaitQueue();
|
||||
}
|
||||
|
||||
public ResourceLimits.Limit endpointLimit()
|
||||
{
|
||||
return allocationObserver.endpoint(delegate.endpointLimit());
|
||||
}
|
||||
|
||||
public AbstractMessageHandler.WaitQueue endpointWaitQueue()
|
||||
{
|
||||
return delegate.endpointWaitQueue();
|
||||
}
|
||||
|
||||
public void release()
|
||||
{
|
||||
delegate.release();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected MessageConsumer<Message.Request> messageConsumer()
|
||||
{
|
||||
return consumer == null ? super.messageConsumer() : consumer;
|
||||
}
|
||||
}
|
||||
|
||||
static class AllocationObserver
|
||||
{
|
||||
volatile InstrumentedLimit endpoint;
|
||||
volatile InstrumentedLimit global;
|
||||
|
||||
long endpointAllocationTotal()
|
||||
{
|
||||
return endpoint == null ? 0 : endpoint.totalAllocated.get();
|
||||
}
|
||||
|
||||
long endpointReleaseTotal()
|
||||
{
|
||||
return endpoint == null ? 0 : endpoint.totalReleased.get();
|
||||
}
|
||||
|
||||
long globalAllocationTotal()
|
||||
{
|
||||
return global == null ? 0 : global.totalAllocated.get();
|
||||
}
|
||||
|
||||
long globalReleaseTotal()
|
||||
{
|
||||
return global == null ? 0 : global.totalReleased.get();
|
||||
}
|
||||
|
||||
synchronized InstrumentedLimit endpoint(ResourceLimits.Limit delegate)
|
||||
{
|
||||
if (endpoint == null)
|
||||
endpoint = new InstrumentedLimit(delegate);
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
synchronized InstrumentedLimit global(ResourceLimits.Limit delegate)
|
||||
{
|
||||
if (global == null)
|
||||
global = new InstrumentedLimit(delegate);
|
||||
return global;
|
||||
}
|
||||
|
||||
LongConsumer verifier()
|
||||
{
|
||||
return totalBytes -> {
|
||||
// verify that we did have to acquire some resources from the global/endpoint reserves
|
||||
assertThat(endpointAllocationTotal()).isEqualTo(totalBytes);
|
||||
assertThat(globalAllocationTotal()).isEqualTo(totalBytes);
|
||||
// and that we released it all
|
||||
assertThat(endpointReleaseTotal()).isEqualTo(totalBytes);
|
||||
assertThat(globalReleaseTotal()).isEqualTo(totalBytes);
|
||||
// assert that we definitely have no outstanding resources acquired from the reserves
|
||||
ClientResourceLimits.Allocator tracker =
|
||||
ClientResourceLimits.getAllocatorForEndpoint(FBUtilities.getJustLocalAddress());
|
||||
assertThat(tracker.endpointUsing()).isEqualTo(0);
|
||||
assertThat(tracker.globallyUsing()).isEqualTo(0);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static class InstrumentedLimit extends DelegatingLimit
|
||||
{
|
||||
AtomicLong totalAllocated = new AtomicLong(0);
|
||||
AtomicLong totalReleased = new AtomicLong(0);
|
||||
|
||||
InstrumentedLimit(ResourceLimits.Limit wrapped)
|
||||
{
|
||||
super(wrapped);
|
||||
}
|
||||
|
||||
public boolean tryAllocate(long amount)
|
||||
{
|
||||
totalAllocated.addAndGet(amount);
|
||||
return super.tryAllocate(amount);
|
||||
}
|
||||
|
||||
public ResourceLimits.Outcome release(long amount)
|
||||
{
|
||||
totalReleased.addAndGet(amount);
|
||||
return super.release(amount);
|
||||
}
|
||||
}
|
||||
|
||||
static class DelegatingLimit implements ResourceLimits.Limit
|
||||
{
|
||||
private final ResourceLimits.Limit wrapped;
|
||||
|
||||
DelegatingLimit(ResourceLimits.Limit wrapped)
|
||||
{
|
||||
this.wrapped = wrapped;
|
||||
}
|
||||
|
||||
public long limit()
|
||||
{
|
||||
return wrapped.limit();
|
||||
}
|
||||
|
||||
public long setLimit(long newLimit)
|
||||
{
|
||||
return wrapped.setLimit(newLimit);
|
||||
}
|
||||
|
||||
public long remaining()
|
||||
{
|
||||
return wrapped.remaining();
|
||||
}
|
||||
|
||||
public long using()
|
||||
{
|
||||
return wrapped.using();
|
||||
}
|
||||
|
||||
public boolean tryAllocate(long amount)
|
||||
{
|
||||
return wrapped.tryAllocate(amount);
|
||||
}
|
||||
|
||||
public void allocate(long amount)
|
||||
{
|
||||
wrapped.allocate(amount);
|
||||
}
|
||||
|
||||
public ResourceLimits.Outcome release(long amount)
|
||||
{
|
||||
return wrapped.release(amount);
|
||||
}
|
||||
}
|
||||
|
||||
static class Codec
|
||||
{
|
||||
final FrameEncoder encoder;
|
||||
final FrameDecoder decoder;
|
||||
Codec(FrameEncoder encoder, FrameDecoder decoder)
|
||||
{
|
||||
this.encoder = encoder;
|
||||
this.decoder = decoder;
|
||||
}
|
||||
|
||||
static Codec lz4(BufferPoolAllocator alloc)
|
||||
{
|
||||
return new Codec(FrameEncoderLZ4.fastInstance, FrameDecoderLZ4.fast(alloc));
|
||||
}
|
||||
|
||||
static Codec crc(BufferPoolAllocator alloc)
|
||||
{
|
||||
return new Codec(FrameEncoderCrc.instance, new FrameDecoderCrc(alloc));
|
||||
}
|
||||
}
|
||||
|
||||
static class Client
|
||||
{
|
||||
private final Codec codec;
|
||||
private Channel channel;
|
||||
final int expectedResponses;
|
||||
final CountDownLatch responsesReceived;
|
||||
private volatile boolean connected = false;
|
||||
|
||||
final Queue<Envelope> inboundMessages = new LinkedBlockingQueue<>();
|
||||
long sendSize = 0;
|
||||
SimpleClient.SimpleFlusher flusher;
|
||||
ErrorMessage connectionError;
|
||||
Throwable disconnectionError;
|
||||
|
||||
Client(Codec codec, int expectedResponses)
|
||||
{
|
||||
this.codec = codec;
|
||||
this.expectedResponses = expectedResponses;
|
||||
this.responsesReceived = new CountDownLatch(expectedResponses);
|
||||
flusher = new SimpleClient.SimpleFlusher(codec.encoder);
|
||||
}
|
||||
|
||||
private void connect(InetAddress address, int port) throws IOException, InterruptedException
|
||||
{
|
||||
final CountDownLatch ready = new CountDownLatch(1);
|
||||
Bootstrap bootstrap = new Bootstrap()
|
||||
.group(new NioEventLoopGroup(0, new NamedThreadFactory("TEST-CLIENT")))
|
||||
.channel(io.netty.channel.socket.nio.NioSocketChannel.class)
|
||||
.option(ChannelOption.TCP_NODELAY, true);
|
||||
bootstrap.handler(new ChannelInitializer<Channel>()
|
||||
{
|
||||
protected void initChannel(Channel channel) throws Exception
|
||||
{
|
||||
BufferPoolAllocator allocator = GlobalBufferPoolAllocator.instance;
|
||||
channel.config().setOption(ChannelOption.ALLOCATOR, allocator);
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
// Outbound handlers to enable us to send the initial STARTUP
|
||||
pipeline.addLast("envelopeEncoder", Envelope.Encoder.instance);
|
||||
pipeline.addLast("messageEncoder", PreV5Handlers.ProtocolEncoder.instance);
|
||||
pipeline.addLast("envelopeDecoder", new Envelope.Decoder());
|
||||
// Inbound handler to perform the handshake & modify the pipeline on receipt of a READY
|
||||
pipeline.addLast("handshake", new MessageToMessageDecoder<Envelope>()
|
||||
{
|
||||
final Envelope.Decoder decoder = new Envelope.Decoder();
|
||||
protected void decode(ChannelHandlerContext ctx, Envelope msg, List<Object> out) throws Exception
|
||||
{
|
||||
// Handle ERROR responses during initial connection and protocol negotiation
|
||||
if ( msg.header.type == Message.Type.ERROR)
|
||||
{
|
||||
connectionError = (ErrorMessage)Message.responseDecoder()
|
||||
.decode(ctx.channel(), msg);
|
||||
|
||||
msg.release();
|
||||
logger.info("ERROR");
|
||||
stop();
|
||||
ready.countDown();
|
||||
return;
|
||||
}
|
||||
|
||||
// As soon as we receive a READY message, modify the pipeline
|
||||
assert msg.header.type == Message.Type.READY;
|
||||
msg.release();
|
||||
|
||||
// just split the messaging into cql messages and stash them for verification
|
||||
FrameDecoder.FrameProcessor processor = frame -> {
|
||||
if (frame instanceof FrameDecoder.IntactFrame)
|
||||
{
|
||||
ByteBuffer bytes = ((FrameDecoder.IntactFrame)frame).contents.get();
|
||||
while(bytes.hasRemaining())
|
||||
{
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(bytes);
|
||||
try
|
||||
{
|
||||
inboundMessages.add(decoder.decode(buffer));
|
||||
responsesReceived.countDown();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new IOException(e);
|
||||
}
|
||||
bytes.position(bytes.position() + buffer.readerIndex());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// for testing purposes, don't actually encode CQL messages,
|
||||
// we supply messaging frames directly to this client
|
||||
channel.pipeline().remove("envelopeEncoder");
|
||||
channel.pipeline().remove("messageEncoder");
|
||||
channel.pipeline().remove("envelopeDecoder");
|
||||
|
||||
// replace this handshake handler with an inbound message frame decoder
|
||||
channel.pipeline().replace(this, "frameDecoder", codec.decoder);
|
||||
// add an outbound message frame encoder
|
||||
channel.pipeline().addLast("frameEncoder", codec.encoder);
|
||||
channel.pipeline().addLast("errorHandler", new ChannelInboundHandlerAdapter()
|
||||
{
|
||||
@Override
|
||||
public void exceptionCaught(final ChannelHandlerContext ctx, Throwable cause) throws Exception
|
||||
{
|
||||
// if the connection is closed finish early as
|
||||
// we don't want to wait for expected responses
|
||||
if (cause instanceof IOException)
|
||||
{
|
||||
connected = false;
|
||||
disconnectionError = cause;
|
||||
int remaining = (int) responsesReceived.getCount();
|
||||
for (int i=0; i < remaining; i++)
|
||||
responsesReceived.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
codec.decoder.activate(processor);
|
||||
connected = true;
|
||||
// Schedule the proto-flusher to collate any messages that have been
|
||||
// written, via enqueue(Envelope message), and flush them to the outbound pipeline
|
||||
flusher.schedule(channel.pipeline().lastContext());
|
||||
ready.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ChannelFuture future = bootstrap.connect(address, port);
|
||||
|
||||
// Wait until the connection attempt succeeds or fails.
|
||||
channel = future.awaitUninterruptibly().channel();
|
||||
if (!future.isSuccess())
|
||||
{
|
||||
bootstrap.group().shutdownGracefully();
|
||||
throw new IOException("Connection Error", future.cause());
|
||||
}
|
||||
|
||||
// Send an initial STARTUP message to kick off the handshake with the server
|
||||
Map<String, String> options = new HashMap<>();
|
||||
options.put(StartupMessage.CQL_VERSION, QueryProcessor.CQL_VERSION.toString());
|
||||
if (codec.encoder instanceof FrameEncoderLZ4)
|
||||
options.put(StartupMessage.COMPRESSION, "LZ4");
|
||||
Connection connection = new Connection(channel, ProtocolVersion.V5, (ch, connection1) -> {});
|
||||
channel.attr(Connection.attributeKey).set(connection);
|
||||
channel.writeAndFlush(new StartupMessage(options)).sync();
|
||||
|
||||
if (!ready.await(10, TimeUnit.SECONDS))
|
||||
throw new RuntimeException("Failed to establish client connection in 10s");
|
||||
}
|
||||
|
||||
void send(Envelope request)
|
||||
{
|
||||
flusher.enqueue(request);
|
||||
sendSize += request.header.bodySizeInBytes;
|
||||
}
|
||||
|
||||
private void awaitResponses() throws InterruptedException
|
||||
{
|
||||
if (!responsesReceived.await(10, TimeUnit.SECONDS))
|
||||
{
|
||||
fail(String.format("Didn't receive all responses, expected %d, actual %d",
|
||||
expectedResponses,
|
||||
inboundMessages.size()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnected()
|
||||
{
|
||||
return connected;
|
||||
}
|
||||
|
||||
private ErrorMessage getConnectionError()
|
||||
{
|
||||
return connectionError;
|
||||
}
|
||||
|
||||
private Envelope pollResponses()
|
||||
{
|
||||
return inboundMessages.poll();
|
||||
}
|
||||
|
||||
private void stop()
|
||||
{
|
||||
if (channel != null && channel.isOpen())
|
||||
channel.close().awaitUninterruptibly();
|
||||
|
||||
flusher.releaseAll();
|
||||
|
||||
Envelope f;
|
||||
while ((f = inboundMessages.poll()) != null)
|
||||
f.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.junit.*;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.db.virtual.*;
|
||||
import org.apache.cassandra.exceptions.OverloadedException;
|
||||
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.transport.messages.QueryMessage;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
import static org.apache.cassandra.Util.spinAssertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ClientResourceLimitsTest extends CQLTester
|
||||
{
|
||||
|
||||
private static final long LOW_LIMIT = 600L;
|
||||
private static final long HIGH_LIMIT = 5000000000L;
|
||||
|
||||
private static final QueryOptions V5_DEFAULT_OPTIONS = QueryOptions.create(
|
||||
QueryOptions.DEFAULT.getConsistency(),
|
||||
QueryOptions.DEFAULT.getValues(),
|
||||
QueryOptions.DEFAULT.skipMetadata(),
|
||||
QueryOptions.DEFAULT.getPageSize(),
|
||||
QueryOptions.DEFAULT.getPagingState(),
|
||||
QueryOptions.DEFAULT.getSerialConsistency(),
|
||||
ProtocolVersion.V5,
|
||||
KEYSPACE);
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp()
|
||||
{
|
||||
DatabaseDescriptor.setNativeTransportReceiveQueueCapacityInBytes(1);
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytesPerIp(LOW_LIMIT);
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytes(LOW_LIMIT);
|
||||
requireNetwork();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown()
|
||||
{
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytesPerIp(3000000000L);
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytes(HIGH_LIMIT);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setLimits()
|
||||
{
|
||||
ClientResourceLimits.setGlobalLimit(LOW_LIMIT);
|
||||
ClientResourceLimits.setEndpointLimit(LOW_LIMIT);
|
||||
}
|
||||
|
||||
@After
|
||||
public void dropCreatedTable()
|
||||
{
|
||||
try
|
||||
{
|
||||
QueryProcessor.executeOnceInternal("DROP TABLE " + KEYSPACE + ".atable");
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
private SimpleClient client(boolean throwOnOverload)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SimpleClient.builder(nativeAddr.getHostAddress(), nativePort)
|
||||
.protocolVersion(ProtocolVersion.V5)
|
||||
.useBeta()
|
||||
.build()
|
||||
.connect(false, throwOnOverload);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException("Error initializing client", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
private SimpleClient client(boolean throwOnOverload, int largeMessageThreshold)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SimpleClient.builder(nativeAddr.getHostAddress(), nativePort)
|
||||
.protocolVersion(ProtocolVersion.V5)
|
||||
.useBeta()
|
||||
.largeMessageThreshold(largeMessageThreshold)
|
||||
.build()
|
||||
.connect(false, throwOnOverload);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException("Error initializing client", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryExecutionWithThrowOnOverload() throws Throwable
|
||||
{
|
||||
testQueryExecution(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryExecutionWithoutThrowOnOverload() throws Throwable
|
||||
{
|
||||
testQueryExecution(false);
|
||||
}
|
||||
|
||||
private void testQueryExecution(boolean throwOnOverload) throws Throwable
|
||||
{
|
||||
try (SimpleClient client = client(throwOnOverload))
|
||||
{
|
||||
QueryMessage queryMessage = new QueryMessage("CREATE TABLE atable (pk int PRIMARY KEY, v text)",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
queryMessage = new QueryMessage("SELECT * FROM atable", V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBackpressureOnGlobalLimitExceeded() throws Throwable
|
||||
{
|
||||
// Bump the per-endpoint limit to make sure we exhaust the global
|
||||
ClientResourceLimits.setEndpointLimit(HIGH_LIMIT);
|
||||
backPressureTest(() -> ClientResourceLimits.setGlobalLimit(HIGH_LIMIT),
|
||||
(provider) -> provider.globalWaitQueue().signal());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBackPressureWhenEndpointLimitExceeded() throws Throwable
|
||||
{
|
||||
// Make sure we can only exceed the per-endpoint limit
|
||||
ClientResourceLimits.setGlobalLimit(HIGH_LIMIT);
|
||||
backPressureTest(() -> ClientResourceLimits.setEndpointLimit(HIGH_LIMIT),
|
||||
(provider) -> provider.endpointWaitQueue().signal());
|
||||
}
|
||||
|
||||
private void backPressureTest(Runnable limitLifter, Consumer<ClientResourceLimits.ResourceProvider> signaller)
|
||||
throws Throwable
|
||||
{
|
||||
final AtomicReference<Exception> error = new AtomicReference<>();
|
||||
final CountDownLatch started = new CountDownLatch(1);
|
||||
final CountDownLatch complete = new CountDownLatch(1);
|
||||
try(SimpleClient client = client(false))
|
||||
{
|
||||
QueryMessage queryMessage = new QueryMessage("CREATE TABLE atable (pk int PRIMARY KEY, v text)",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
|
||||
// There should be no paused client connections yet
|
||||
Gauge<Integer> pausedConnections = getPausedConnectionsGauge();
|
||||
int before = pausedConnections.getValue();
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
try
|
||||
{
|
||||
started.countDown();
|
||||
client.execute(queryMessage());
|
||||
complete.countDown();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// if backpressure blocks the request execution for too long
|
||||
// (10 seconds per SimpleClient), we'll catch a timeout exception
|
||||
error.set(e);
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
|
||||
// When the client attempts to execute the second query, the backpressure
|
||||
// mechanism should report the client connection is paused
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
spinAssertEquals("Timed out after waiting 5 seconds for paused " +
|
||||
"connections metric to increment due to backpressure",
|
||||
before + 1, pausedConnections::getValue, 5, TimeUnit.SECONDS);
|
||||
|
||||
// verify the request hasn't completed
|
||||
assertFalse(complete.await(1, TimeUnit.SECONDS));
|
||||
|
||||
// backpressure has been applied, if we increase the limits of the exhausted reserve and signal
|
||||
// the appropriate WaitQueue, it should be released and the client request will complete
|
||||
limitLifter.run();
|
||||
// We need a ResourceProvider to get access to the WaitQueue
|
||||
ClientResourceLimits.Allocator allocator = ClientResourceLimits.getAllocatorForEndpoint(FBUtilities.getJustLocalAddress());
|
||||
ClientResourceLimits.ResourceProvider queueHandle = new ClientResourceLimits.ResourceProvider.Default(allocator);
|
||||
signaller.accept(queueHandle);
|
||||
|
||||
// SimpleClient has a 10 second timeout, so if we have to wait
|
||||
// longer than that assume that we're not going to receive a
|
||||
// reply. If all's well, the completion should happen immediately
|
||||
assertTrue(complete.await(11, TimeUnit.SECONDS));
|
||||
assertNull(error.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverloadedExceptionWhenGlobalLimitExceeded() throws Throwable
|
||||
{
|
||||
// Bump the per-endpoint limit to make sure we exhaust the global
|
||||
ClientResourceLimits.setEndpointLimit(HIGH_LIMIT);
|
||||
testOverloadedException(() -> client(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverloadedExceptionWhenEndpointLimitExceeded() throws Throwable
|
||||
{
|
||||
// Make sure we can only exceed the per-endpoint limit
|
||||
ClientResourceLimits.setGlobalLimit(HIGH_LIMIT);
|
||||
testOverloadedException(() -> client(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverloadedExceptionWhenGlobalLimitByMultiFrameMessage() throws Throwable
|
||||
{
|
||||
// Bump the per-endpoint limit to make sure we exhaust the global
|
||||
ClientResourceLimits.setEndpointLimit(HIGH_LIMIT);
|
||||
testOverloadedException(() -> client(true, Ints.checkedCast(LOW_LIMIT / 2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverloadedExceptionWhenEndpointLimitByMultiFrameMessage() throws Throwable
|
||||
{
|
||||
// Make sure we can only exceed the per-endpoint limit
|
||||
ClientResourceLimits.setGlobalLimit(HIGH_LIMIT);
|
||||
testOverloadedException(() -> client(true, Ints.checkedCast(LOW_LIMIT / 2)));
|
||||
}
|
||||
|
||||
private void testOverloadedException(Supplier<SimpleClient> clientSupplier)
|
||||
{
|
||||
try (SimpleClient client = clientSupplier.get())
|
||||
{
|
||||
QueryMessage queryMessage = new QueryMessage("CREATE TABLE atable (pk int PRIMARY KEY, v text)",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
|
||||
queryMessage = queryMessage();
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage);
|
||||
fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private QueryMessage queryMessage()
|
||||
{
|
||||
StringBuilder query = new StringBuilder("INSERT INTO atable (pk, v) VALUES (1, '");
|
||||
for (int i=0; i < LOW_LIMIT * 2; i++)
|
||||
query.append('a');
|
||||
query.append("')");
|
||||
return new QueryMessage(query.toString(), V5_DEFAULT_OPTIONS);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Gauge<Integer> getPausedConnectionsGauge()
|
||||
{
|
||||
String metricName = "org.apache.cassandra.metrics.Client.PausedConnections";
|
||||
Map<String, Gauge> metrics = CassandraMetricsRegistry.Metrics.getGauges((name, metric) -> name.equals(metricName));
|
||||
if (metrics.size() != 1)
|
||||
fail(String.format("Expected a single registered metric for paused client connections, found %s",
|
||||
metrics.size()));
|
||||
return metrics.get(metricName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryUpdatesConcurrentMetricsUpdate() throws Throwable
|
||||
{
|
||||
try (SimpleClient client = client(true))
|
||||
{
|
||||
CyclicBarrier barrier = new CyclicBarrier(2);
|
||||
String table = createTableName();
|
||||
|
||||
// reusing table name for keyspace name since cannot reuse KEYSPACE and want it to be unique
|
||||
TableMetadata tableMetadata =
|
||||
TableMetadata.builder(table, table)
|
||||
.kind(TableMetadata.Kind.VIRTUAL)
|
||||
.addPartitionKeyColumn("pk", UTF8Type.instance)
|
||||
.addRegularColumn("v", Int32Type.instance)
|
||||
.build();
|
||||
|
||||
VirtualTable vt1 = new AbstractVirtualTable.SimpleTable(tableMetadata, () -> {
|
||||
try
|
||||
{
|
||||
// sync up with main thread thats waiting for query to be in progress
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
// wait until metric has been checked
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignore interuption and barrier exceptions
|
||||
}
|
||||
return new SimpleDataSet(tableMetadata);
|
||||
});
|
||||
VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(table, ImmutableList.of(vt1)));
|
||||
|
||||
final QueryMessage queryMessage = new QueryMessage(String.format("SELECT * FROM %s.%s", table, table),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
|
||||
Assert.assertEquals(0L, ClientResourceLimits.getCurrentGlobalUsage());
|
||||
try
|
||||
{
|
||||
Thread tester = new Thread(() -> client.execute(queryMessage));
|
||||
tester.setDaemon(true); // so wont block exit if something fails
|
||||
tester.start();
|
||||
// block until query in progress
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
assertTrue(ClientResourceLimits.getCurrentGlobalUsage() > 0);
|
||||
} finally
|
||||
{
|
||||
// notify query thread that metric has been checked. This will also throw TimeoutException if both
|
||||
// the query threads barriers are not reached
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangingLimitsAtRuntime() throws Throwable
|
||||
{
|
||||
SimpleClient client = client(true);
|
||||
try
|
||||
{
|
||||
QueryMessage smallMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk int PRIMARY KEY, v text)", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(smallMessage);
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage());
|
||||
fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// change global limit, query will still fail because endpoint limit
|
||||
ClientResourceLimits.setGlobalLimit(HIGH_LIMIT);
|
||||
Assert.assertEquals("new global limit not returned by EndpointPayloadTrackers", HIGH_LIMIT, ClientResourceLimits.getGlobalLimit());
|
||||
Assert.assertEquals("new global limit not returned by DatabaseDescriptor", HIGH_LIMIT, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytes());
|
||||
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage());
|
||||
fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// change endpoint limit, query will now succeed
|
||||
ClientResourceLimits.setEndpointLimit(HIGH_LIMIT);
|
||||
Assert.assertEquals("new endpoint limit not returned by EndpointPayloadTrackers", HIGH_LIMIT, ClientResourceLimits.getEndpointLimit());
|
||||
Assert.assertEquals("new endpoint limit not returned by DatabaseDescriptor", HIGH_LIMIT, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp());
|
||||
client.execute(queryMessage());
|
||||
|
||||
// ensure new clients also see the new raised limits
|
||||
client.close();
|
||||
client = client(true);
|
||||
client.execute(queryMessage());
|
||||
|
||||
// lower the global limit and ensure the query fails again
|
||||
ClientResourceLimits.setGlobalLimit(LOW_LIMIT);
|
||||
Assert.assertEquals("new global limit not returned by EndpointPayloadTrackers", LOW_LIMIT, ClientResourceLimits.getGlobalLimit());
|
||||
Assert.assertEquals("new global limit not returned by DatabaseDescriptor", LOW_LIMIT, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytes());
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage());
|
||||
fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// lower the endpoint limit and ensure existing clients also have requests that fail
|
||||
ClientResourceLimits.setEndpointLimit(60);
|
||||
Assert.assertEquals("new endpoint limit not returned by EndpointPayloadTrackers", 60, ClientResourceLimits.getEndpointLimit());
|
||||
Assert.assertEquals("new endpoint limit not returned by DatabaseDescriptor", 60, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp());
|
||||
try
|
||||
{
|
||||
client.execute(smallMessage);
|
||||
fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// ensure new clients also see the new lowered limit
|
||||
client.close();
|
||||
client = client(true);
|
||||
try
|
||||
{
|
||||
client.execute(smallMessage);
|
||||
fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// put the test state back
|
||||
ClientResourceLimits.setEndpointLimit(LOW_LIMIT);
|
||||
Assert.assertEquals("new endpoint limit not returned by EndpointPayloadTrackers", LOW_LIMIT, ClientResourceLimits.getEndpointLimit());
|
||||
Assert.assertEquals("new endpoint limit not returned by DatabaseDescriptor", LOW_LIMIT, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp());
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,378 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.apache.cassandra.OrderedJUnit4ClassRunner;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.db.virtual.AbstractVirtualTable;
|
||||
import org.apache.cassandra.db.virtual.SimpleDataSet;
|
||||
import org.apache.cassandra.db.virtual.VirtualKeyspace;
|
||||
import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
|
||||
import org.apache.cassandra.db.virtual.VirtualTable;
|
||||
import org.apache.cassandra.exceptions.OverloadedException;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.transport.messages.QueryMessage;
|
||||
|
||||
@RunWith(OrderedJUnit4ClassRunner.class)
|
||||
public class InflightRequestPayloadTrackerTest extends CQLTester
|
||||
{
|
||||
|
||||
private static long LOW_LIMIT = 600L;
|
||||
private static long HIGH_LIMIT = 5000000000L;
|
||||
|
||||
private static final QueryOptions V5_DEFAULT_OPTIONS = QueryOptions.create(
|
||||
QueryOptions.DEFAULT.getConsistency(),
|
||||
QueryOptions.DEFAULT.getValues(),
|
||||
QueryOptions.DEFAULT.skipMetadata(),
|
||||
QueryOptions.DEFAULT.getPageSize(),
|
||||
QueryOptions.DEFAULT.getPagingState(),
|
||||
QueryOptions.DEFAULT.getSerialConsistency(),
|
||||
ProtocolVersion.V5,
|
||||
KEYSPACE);
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp()
|
||||
{
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytesPerIp(600);
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytes(600);
|
||||
requireNetwork();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown()
|
||||
{
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytesPerIp(3000000000L);
|
||||
DatabaseDescriptor.setNativeTransportMaxConcurrentRequestsInBytes(HIGH_LIMIT);
|
||||
}
|
||||
|
||||
@After
|
||||
public void dropCreatedTable()
|
||||
{
|
||||
try
|
||||
{
|
||||
QueryProcessor.executeOnceInternal("DROP TABLE " + KEYSPACE + ".atable");
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private SimpleClient client() throws IOException
|
||||
{
|
||||
return new SimpleClient(nativeAddr.getHostAddress(),
|
||||
nativePort,
|
||||
ProtocolVersion.V5,
|
||||
true,
|
||||
new EncryptionOptions())
|
||||
.connect(false, false, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryExecutionWithThrowOnOverload() throws Throwable
|
||||
{
|
||||
try (SimpleClient client = client())
|
||||
{
|
||||
QueryMessage queryMessage = new QueryMessage("CREATE TABLE atable (pk1 int PRIMARY KEY, v text)",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryExecutionWithoutThrowOnOverload() throws Throwable
|
||||
{
|
||||
try (SimpleClient client = client())
|
||||
{
|
||||
client.connect(false, false, false);
|
||||
|
||||
QueryMessage queryMessage = new QueryMessage("CREATE TABLE atable (pk int PRIMARY KEY, v text)",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
queryMessage = new QueryMessage("SELECT * FROM atable",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryUpdatesConcurrentMetricsUpdate() throws Throwable
|
||||
{
|
||||
try (SimpleClient client = client())
|
||||
{
|
||||
CyclicBarrier barrier = new CyclicBarrier(2);
|
||||
String table = createTableName();
|
||||
|
||||
// reusing table name for keyspace name since cannot reuse KEYSPACE and want it to be unique
|
||||
TableMetadata tableMetadata =
|
||||
TableMetadata.builder(table, table)
|
||||
.kind(TableMetadata.Kind.VIRTUAL)
|
||||
.addPartitionKeyColumn("pk", UTF8Type.instance)
|
||||
.addRegularColumn("v", Int32Type.instance)
|
||||
.build();
|
||||
|
||||
VirtualTable vt1 = new AbstractVirtualTable.SimpleTable(tableMetadata, () -> {
|
||||
try
|
||||
{
|
||||
// sync up with main thread thats waiting for query to be in progress
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
// wait until metric has been checked
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignore interuption and barrier exceptions
|
||||
}
|
||||
return new SimpleDataSet(tableMetadata);
|
||||
});
|
||||
VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(table, ImmutableList.of(vt1)));
|
||||
|
||||
final QueryMessage queryMessage = new QueryMessage(String.format("SELECT * FROM %s.%s", table, table),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
|
||||
Assert.assertEquals(0L, Server.EndpointPayloadTracker.getCurrentGlobalUsage());
|
||||
try
|
||||
{
|
||||
Thread tester = new Thread(() -> client.execute(queryMessage));
|
||||
tester.setDaemon(true); // so wont block exit if something fails
|
||||
tester.start();
|
||||
// block until query in progress
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
Assert.assertTrue(Server.EndpointPayloadTracker.getCurrentGlobalUsage() > 0);
|
||||
} finally
|
||||
{
|
||||
// notify query thread that metric has been checked. This will also throw TimeoutException if both
|
||||
// the query threads barriers are not reached
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryExecutionWithoutThrowOnOverloadAndInflightLimitedExceeded() throws Throwable
|
||||
{
|
||||
try (SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(),
|
||||
nativePort,
|
||||
ProtocolVersion.V5,
|
||||
true,
|
||||
new EncryptionOptions()))
|
||||
{
|
||||
client.connect(false, false, false);
|
||||
QueryMessage queryMessage = new QueryMessage("CREATE TABLE atable (pk int PRIMARY KEY, v text)",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
|
||||
queryMessage = new QueryMessage("INSERT INTO atable (pk, v) VALUES (1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverloadedExceptionForEndpointInflightLimit() throws Throwable
|
||||
{
|
||||
try (SimpleClient client = client())
|
||||
{
|
||||
QueryMessage queryMessage = new QueryMessage("CREATE TABLE atable (pk int PRIMARY KEY, v text)",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
|
||||
queryMessage = new QueryMessage("INSERT INTO atable (pk, v) VALUES (1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverloadedExceptionForOverallInflightLimit() throws Throwable
|
||||
{
|
||||
try (SimpleClient client = client())
|
||||
{
|
||||
QueryMessage queryMessage = new QueryMessage("CREATE TABLE atable (pk int PRIMARY KEY, v text)",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
|
||||
queryMessage = new QueryMessage("INSERT INTO atable (pk, v) VALUES (1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')",
|
||||
V5_DEFAULT_OPTIONS);
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangingLimitsAtRuntime() throws Throwable
|
||||
{
|
||||
SimpleClient client = client();
|
||||
try
|
||||
{
|
||||
QueryMessage queryMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk int PRIMARY KEY, v text)", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
|
||||
queryMessage = new QueryMessage(String.format("INSERT INTO %s.atable (pk, v) VALUES (1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// change global limit, query will still fail because endpoint limit
|
||||
Server.EndpointPayloadTracker.setGlobalLimit(HIGH_LIMIT);
|
||||
Assert.assertEquals("new global limit not returned by EndpointPayloadTrackers", HIGH_LIMIT, Server.EndpointPayloadTracker.getGlobalLimit());
|
||||
Assert.assertEquals("new global limit not returned by DatabaseDescriptor", HIGH_LIMIT, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytes());
|
||||
|
||||
queryMessage = new QueryMessage(String.format("INSERT INTO %s.atable (pk, v) VALUES (1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// change endpoint limit, query will still now succeed
|
||||
Server.EndpointPayloadTracker.setEndpointLimit(HIGH_LIMIT);
|
||||
Assert.assertEquals("new endpoint limit not returned by EndpointPayloadTrackers", HIGH_LIMIT, Server.EndpointPayloadTracker.getEndpointLimit());
|
||||
Assert.assertEquals("new endpoint limit not returned by DatabaseDescriptor", HIGH_LIMIT, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp());
|
||||
|
||||
queryMessage = new QueryMessage(String.format("INSERT INTO %s.atable (pk, v) VALUES (1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
|
||||
// ensure new clients also see the new raised limits
|
||||
client.close();
|
||||
client = new SimpleClient(nativeAddr.getHostAddress(),
|
||||
nativePort,
|
||||
ProtocolVersion.V5,
|
||||
true,
|
||||
new EncryptionOptions());
|
||||
client.connect(false, true, true);
|
||||
|
||||
queryMessage = new QueryMessage(String.format("INSERT INTO %s.atable (pk, v) VALUES (1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
client.execute(queryMessage);
|
||||
|
||||
// lower the global limit and ensure the query fails again
|
||||
Server.EndpointPayloadTracker.setGlobalLimit(LOW_LIMIT);
|
||||
Assert.assertEquals("new global limit not returned by EndpointPayloadTrackers", LOW_LIMIT, Server.EndpointPayloadTracker.getGlobalLimit());
|
||||
Assert.assertEquals("new global limit not returned by DatabaseDescriptor", LOW_LIMIT, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytes());
|
||||
|
||||
queryMessage = new QueryMessage(String.format("INSERT INTO %s.atable (pk, v) VALUES (1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// lower the endpoint limit and ensure existing clients also have requests that fail
|
||||
Server.EndpointPayloadTracker.setEndpointLimit(60);
|
||||
Assert.assertEquals("new endpoint limit not returned by EndpointPayloadTrackers", 60, Server.EndpointPayloadTracker.getEndpointLimit());
|
||||
Assert.assertEquals("new endpoint limit not returned by DatabaseDescriptor", 60, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp());
|
||||
|
||||
queryMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk int PRIMARY KEY, v text)", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// ensure new clients also see the new lowered limit
|
||||
client.close();
|
||||
client = new SimpleClient(nativeAddr.getHostAddress(),
|
||||
nativePort,
|
||||
ProtocolVersion.V5,
|
||||
true,
|
||||
new EncryptionOptions());
|
||||
client.connect(false, true, true);
|
||||
|
||||
queryMessage = new QueryMessage(String.format("CREATE TABLE %s.atable (pk int PRIMARY KEY, v text)", KEYSPACE),
|
||||
V5_DEFAULT_OPTIONS);
|
||||
try
|
||||
{
|
||||
client.execute(queryMessage);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertTrue(e.getCause() instanceof OverloadedException);
|
||||
}
|
||||
|
||||
// put the test state back
|
||||
Server.EndpointPayloadTracker.setEndpointLimit(LOW_LIMIT);
|
||||
Assert.assertEquals("new endpoint limit not returned by EndpointPayloadTrackers", LOW_LIMIT, Server.EndpointPayloadTracker.getEndpointLimit());
|
||||
Assert.assertEquals("new endpoint limit not returned by DatabaseDescriptor", LOW_LIMIT, DatabaseDescriptor.getNativeTransportMaxConcurrentRequestsInBytesPerIp());
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@ public class MessagePayloadTest extends CQLTester
|
|||
new EncryptionOptions());
|
||||
try
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
|
||||
Map<String, ByteBuffer> reqMap;
|
||||
Map<String, ByteBuffer> respMap;
|
||||
|
|
@ -205,7 +205,7 @@ public class MessagePayloadTest extends CQLTester
|
|||
SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort);
|
||||
try
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
|
||||
Map<String, ByteBuffer> reqMap;
|
||||
Map<String, ByteBuffer> respMap;
|
||||
|
|
@ -274,7 +274,7 @@ public class MessagePayloadTest extends CQLTester
|
|||
SimpleClient client = new SimpleClient(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V3);
|
||||
try
|
||||
{
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
|
||||
Map<String, ByteBuffer> reqMap;
|
||||
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ public class ProtocolErrorTest {
|
|||
|
||||
public void testInvalidProtocolVersion(int version) throws Exception
|
||||
{
|
||||
Frame.Decoder dec = new Frame.Decoder(null);
|
||||
Envelope.Decoder dec = new Envelope.Decoder();
|
||||
|
||||
List<Object> results = new ArrayList<>();
|
||||
byte[] frame = new byte[] {
|
||||
byte[] bytes = new byte[] {
|
||||
(byte) REQUEST.addToVersion(version), // direction & version
|
||||
0x00, // flags
|
||||
0x00, 0x01, // stream ID
|
||||
|
|
@ -67,7 +67,7 @@ public class ProtocolErrorTest {
|
|||
0x65, 0x6d, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c,
|
||||
0x3b
|
||||
};
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(frame);
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
|
||||
try {
|
||||
dec.decode(null, buf, results);
|
||||
Assert.fail("Expected protocol error");
|
||||
|
|
@ -77,20 +77,20 @@ public class ProtocolErrorTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidProtocolVersionShortFrame() throws Exception
|
||||
public void testInvalidProtocolVersionShortBody() throws Exception
|
||||
{
|
||||
// test for CASSANDRA-11464
|
||||
Frame.Decoder dec = new Frame.Decoder(null);
|
||||
Envelope.Decoder dec = new Envelope.Decoder();
|
||||
|
||||
List<Object> results = new ArrayList<>();
|
||||
byte[] frame = new byte[] {
|
||||
byte[] bytes = new byte[] {
|
||||
(byte) REQUEST.addToVersion(1), // direction & version
|
||||
0x00, // flags
|
||||
0x01, // stream ID
|
||||
0x09, // opcode
|
||||
0x00, 0x00, 0x00, 0x21, // body length
|
||||
};
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(frame);
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
|
||||
try {
|
||||
dec.decode(null, buf, results);
|
||||
Assert.fail("Expected protocol error");
|
||||
|
|
@ -102,12 +102,12 @@ public class ProtocolErrorTest {
|
|||
@Test
|
||||
public void testInvalidDirection() throws Exception
|
||||
{
|
||||
Frame.Decoder dec = new Frame.Decoder(null);
|
||||
Envelope.Decoder dec = new Envelope.Decoder();
|
||||
|
||||
List<Object> results = new ArrayList<>();
|
||||
// should generate a protocol exception for using a response frame with
|
||||
// should generate a protocol exception for using a response with
|
||||
// a prepare op, ensure that it comes back with stream ID 1
|
||||
byte[] frame = new byte[] {
|
||||
byte[] bytes = new byte[] {
|
||||
(byte) RESPONSE.addToVersion(ProtocolVersion.CURRENT.asInt()), // direction & version
|
||||
0x00, // flags
|
||||
0x00, 0x01, // stream ID
|
||||
|
|
@ -119,7 +119,7 @@ public class ProtocolErrorTest {
|
|||
0x65, 0x6d, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c,
|
||||
0x3b
|
||||
};
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(frame);
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
|
||||
try {
|
||||
dec.decode(null, buf, results);
|
||||
Assert.fail("Expected protocol error");
|
||||
|
|
@ -133,10 +133,10 @@ public class ProtocolErrorTest {
|
|||
@Test
|
||||
public void testBodyLengthOverLimit() throws Exception
|
||||
{
|
||||
Frame.Decoder dec = new Frame.Decoder(null);
|
||||
Envelope.Decoder dec = new Envelope.Decoder();
|
||||
|
||||
List<Object> results = new ArrayList<>();
|
||||
byte[] frame = new byte[] {
|
||||
byte[] bytes = new byte[] {
|
||||
(byte) REQUEST.addToVersion(ProtocolVersion.CURRENT.asInt()), // direction & version
|
||||
0x00, // flags
|
||||
0x00, 0x01, // stream ID
|
||||
|
|
@ -144,7 +144,7 @@ public class ProtocolErrorTest {
|
|||
0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, // body length
|
||||
};
|
||||
byte[] body = new byte[0x10000000];
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(frame, body);
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(bytes, body);
|
||||
try {
|
||||
dec.decode(null, buf, results);
|
||||
Assert.fail("Expected protocol error");
|
||||
|
|
@ -156,7 +156,7 @@ public class ProtocolErrorTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testErrorMessageWithNullString() throws Exception
|
||||
public void testErrorMessageWithNullString()
|
||||
{
|
||||
// test for CASSANDRA-11167
|
||||
ErrorMessage msg = ErrorMessage.fromException(new ServerError((String) null));
|
||||
|
|
@ -176,18 +176,18 @@ public class ProtocolErrorTest {
|
|||
@Test
|
||||
public void testUnsupportedMessage() throws Exception
|
||||
{
|
||||
byte[] incomingFrame = new byte[] {
|
||||
(byte) REQUEST.addToVersion(ProtocolVersion.CURRENT.asInt()), // direction & version
|
||||
0x00, // flags
|
||||
0x00, 0x01, // stream ID
|
||||
0x04, // opcode for obsoleted CREDENTIALS message
|
||||
0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10, // body length
|
||||
byte[] bytes = new byte[] {
|
||||
(byte) REQUEST.addToVersion(ProtocolVersion.CURRENT.asInt()), // direction & version
|
||||
0x00, // flags
|
||||
0x00, 0x01, // stream ID
|
||||
0x04, // opcode for obsoleted CREDENTIALS message
|
||||
0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10, // body length
|
||||
};
|
||||
byte[] body = new byte[0x10];
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(incomingFrame, body);
|
||||
Frame decodedFrame = new Frame.Decoder(null).decodeFrame(buf);
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(bytes, body);
|
||||
Envelope decoded = new Envelope.Decoder().decode(buf);
|
||||
try {
|
||||
decodedFrame.header.type.codec.decode(decodedFrame.body, decodedFrame.header.version);
|
||||
decoded.header.type.codec.decode(decoded.body, decoded.header.version);
|
||||
Assert.fail("Expected protocol error");
|
||||
} catch (ProtocolException e) {
|
||||
Assert.assertTrue(e.getMessage().contains("Unsupported message"));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.ProtocolVersion;
|
||||
import com.datastax.driver.core.Session;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ProtocolNegotiationTest extends CQLTester
|
||||
{
|
||||
// to avoid JMX naming clashes between cluster metrics
|
||||
private int clusterId = 0;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup()
|
||||
{
|
||||
requireNetwork();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverSupportsV3AndV4ByDefault()
|
||||
{
|
||||
reinitializeNetwork();
|
||||
// client can explicitly request either V3 or V4
|
||||
testConnection(ProtocolVersion.V3, ProtocolVersion.V3);
|
||||
testConnection(ProtocolVersion.V4, ProtocolVersion.V4);
|
||||
|
||||
// if not specified, V4 is the default
|
||||
testConnection(null, ProtocolVersion.V4);
|
||||
testConnection(ProtocolVersion.NEWEST_SUPPORTED, ProtocolVersion.V4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportV5ConnectionWithBetaOption()
|
||||
{
|
||||
reinitializeNetwork();
|
||||
testConnection(ProtocolVersion.V5, ProtocolVersion.V5);
|
||||
testConnection(ProtocolVersion.NEWEST_BETA, ProtocolVersion.V5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void olderVersionsAreUnsupported()
|
||||
{
|
||||
reinitializeNetwork();
|
||||
testConnection(ProtocolVersion.V1, ProtocolVersion.V4);
|
||||
testConnection(ProtocolVersion.V2, ProtocolVersion.V4);
|
||||
}
|
||||
|
||||
private void testConnection(com.datastax.driver.core.ProtocolVersion requestedVersion,
|
||||
com.datastax.driver.core.ProtocolVersion expectedVersion)
|
||||
{
|
||||
boolean expectError = requestedVersion != null && requestedVersion != expectedVersion;
|
||||
Cluster.Builder builder = Cluster.builder()
|
||||
.addContactPoints(nativeAddr)
|
||||
.withClusterName("Test Cluster" + clusterId++)
|
||||
.withPort(nativePort);
|
||||
|
||||
if (requestedVersion != null)
|
||||
{
|
||||
if (requestedVersion.toInt() > org.apache.cassandra.transport.ProtocolVersion.CURRENT.asInt())
|
||||
builder = builder.allowBetaProtocolVersion();
|
||||
else
|
||||
builder = builder.withProtocolVersion(requestedVersion);
|
||||
}
|
||||
|
||||
Cluster cluster = builder.build();
|
||||
try (Session session = cluster.connect())
|
||||
{
|
||||
if (expectError)
|
||||
fail("Expected a protocol exception");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!expectError)
|
||||
{
|
||||
e.printStackTrace();
|
||||
fail("Did not expect any exception");
|
||||
}
|
||||
e.printStackTrace();
|
||||
assertTrue(e.getMessage().contains(String.format("Host does not support protocol version %s", requestedVersion)));
|
||||
} finally {
|
||||
cluster.closeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.transport.messages.StartupMessage;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class StartupMessageTest extends CQLTester
|
||||
{
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp()
|
||||
{
|
||||
requireNetwork();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checksumOptionValidation()
|
||||
{
|
||||
testConnection("crc32", false);
|
||||
testConnection("CRC32", false);
|
||||
testConnection("cRc32", false);
|
||||
testConnection("adler32", false);
|
||||
testConnection("ADLER32", false);
|
||||
testConnection("aDlEr32", false);
|
||||
testConnection("nonesuchtype", true);
|
||||
testConnection("", true);
|
||||
// special case of no option supplied
|
||||
testConnection(null, false);
|
||||
}
|
||||
|
||||
private void testConnection(String checksumType, boolean expectProtocolError)
|
||||
{
|
||||
try (TestClient client = new TestClient(checksumType))
|
||||
{
|
||||
client.connect();
|
||||
if (expectProtocolError)
|
||||
fail("Expected a protocol exception");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!expectProtocolError)
|
||||
fail("Did not expect any exception");
|
||||
|
||||
// This is a bit ugly, but SimpleClient::execute throws RuntimeException if it receives any ErrorMessage
|
||||
String expected = String.format("org.apache.cassandra.transport.ProtocolException: " +
|
||||
"Requested checksum type %s is not known or supported " +
|
||||
"by this version of Cassandra", checksumType);
|
||||
assertEquals(expected, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static class TestClient extends SimpleClient
|
||||
{
|
||||
private final String checksumType;
|
||||
TestClient(String checksumType)
|
||||
{
|
||||
super(nativeAddr.getHostAddress(), nativePort, ProtocolVersion.V5, true, new EncryptionOptions());
|
||||
this.checksumType = checksumType;
|
||||
}
|
||||
|
||||
void connect() throws IOException
|
||||
{
|
||||
establishConnection();
|
||||
Map<String, String> options = new HashMap<>();
|
||||
options.put(StartupMessage.CQL_VERSION, QueryProcessor.CQL_VERSION.toString());
|
||||
|
||||
if (checksumType != null)
|
||||
options.put(StartupMessage.CHECKSUM, checksumType);
|
||||
|
||||
execute(new StartupMessage(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame.checksum;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Random;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.transport.Frame;
|
||||
import org.apache.cassandra.transport.ProtocolException;
|
||||
import org.apache.cassandra.transport.frame.compress.Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.LZ4Compressor;
|
||||
import org.apache.cassandra.transport.frame.compress.SnappyCompressor;
|
||||
import org.apache.cassandra.utils.ChecksumType;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
import org.quicktheories.core.Gen;
|
||||
import org.quicktheories.impl.Constraint;
|
||||
|
||||
import static org.quicktheories.QuickTheory.qt;
|
||||
import static org.quicktheories.generators.SourceDSL.arbitrary;
|
||||
import static org.quicktheories.generators.SourceDSL.integers;
|
||||
|
||||
public class ChecksummingTransformerTest
|
||||
{
|
||||
private static final int DEFAULT_BLOCK_SIZE = 1 << 15;
|
||||
private static final int MAX_INPUT_SIZE = 1 << 18;
|
||||
private static final EnumSet<Frame.Header.Flag> FLAGS = EnumSet.of(Frame.Header.Flag.COMPRESSED, Frame.Header.Flag.CHECKSUMMED);
|
||||
|
||||
@BeforeClass
|
||||
public static void init()
|
||||
{
|
||||
// required as static ChecksummingTransformer instances read default block size from config
|
||||
DatabaseDescriptor.clientInitialization();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripSafetyProperty()
|
||||
{
|
||||
qt()
|
||||
.forAll(inputs(),
|
||||
compressors(),
|
||||
checksumTypes(),
|
||||
blocksizes())
|
||||
.checkAssert(this::roundTrip);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripZeroLengthInput()
|
||||
{
|
||||
qt()
|
||||
.forAll(zeroLengthInputs(),
|
||||
compressors(),
|
||||
checksumTypes(),
|
||||
blocksizes())
|
||||
.checkAssert(this::roundTrip);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corruptionCausesFailure()
|
||||
{
|
||||
qt()
|
||||
.forAll(inputWithCorruptablePosition(),
|
||||
integers().between(0, Byte.MAX_VALUE).map(Integer::byteValue),
|
||||
compressors(),
|
||||
checksumTypes())
|
||||
.checkAssert(ChecksummingTransformerTest::roundTripWithCorruption);
|
||||
}
|
||||
|
||||
static void roundTripWithCorruption(Pair<ReusableBuffer, Integer> inputAndCorruptablePosition,
|
||||
byte corruptionValue,
|
||||
Compressor compressor,
|
||||
ChecksumType checksum)
|
||||
{
|
||||
ReusableBuffer input = inputAndCorruptablePosition.left;
|
||||
ByteBuf expectedBuf = input.toByteBuf();
|
||||
int byteToCorrupt = inputAndCorruptablePosition.right;
|
||||
ChecksummingTransformer transformer = new ChecksummingTransformer(checksum, DEFAULT_BLOCK_SIZE, compressor);
|
||||
ByteBuf outbound = transformer.transformOutbound(expectedBuf);
|
||||
|
||||
// make sure we're actually expecting to produce some corruption
|
||||
if (outbound.getByte(byteToCorrupt) == corruptionValue)
|
||||
return;
|
||||
|
||||
if (byteToCorrupt >= outbound.writerIndex())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
int oldIndex = outbound.writerIndex();
|
||||
outbound.writerIndex(byteToCorrupt);
|
||||
outbound.writeByte(corruptionValue);
|
||||
outbound.writerIndex(oldIndex);
|
||||
ByteBuf inbound = transformer.transformInbound(outbound, FLAGS);
|
||||
|
||||
// verify that the content was actually corrupted
|
||||
expectedBuf.readerIndex(0);
|
||||
Assert.assertEquals(expectedBuf, inbound);
|
||||
} catch(ProtocolException e)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithSingleUncompressableChunk()
|
||||
{
|
||||
byte[] bytes = new byte[]{1};
|
||||
ChecksummingTransformer transformer = new ChecksummingTransformer(ChecksumType.CRC32, DEFAULT_BLOCK_SIZE, LZ4Compressor.INSTANCE);
|
||||
ByteBuf expectedBuf = Unpooled.wrappedBuffer(bytes);
|
||||
|
||||
ByteBuf outbound = transformer.transformOutbound(expectedBuf);
|
||||
ByteBuf inbound = transformer.transformInbound(outbound, FLAGS);
|
||||
|
||||
// reset reader index on expectedBuf back to 0 as it will have been entirely consumed by the transformOutbound() call
|
||||
expectedBuf.readerIndex(0);
|
||||
Assert.assertEquals(expectedBuf, inbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripWithCompressableAndUncompressableChunks() throws IOException
|
||||
{
|
||||
Compressor compressor = LZ4Compressor.INSTANCE;
|
||||
Random random = new Random();
|
||||
int inputLen = 127;
|
||||
|
||||
byte[] uncompressable = new byte[inputLen];
|
||||
for (int i = 0; i < uncompressable.length; i++)
|
||||
uncompressable[i] = (byte) random.nextInt(127);
|
||||
|
||||
byte[] compressed = new byte[compressor.maxCompressedLength(uncompressable.length)];
|
||||
Assert.assertTrue(compressor.compress(uncompressable, 0, uncompressable.length, compressed, 0) > uncompressable.length);
|
||||
|
||||
byte[] compressable = new byte[inputLen];
|
||||
for (int i = 0; i < compressable.length; i++)
|
||||
compressable[i] = (byte)1;
|
||||
Assert.assertTrue(compressor.compress(compressable, 0, compressable.length, compressable, 0) < compressable.length);
|
||||
|
||||
ChecksummingTransformer transformer = new ChecksummingTransformer(ChecksumType.CRC32, uncompressable.length, LZ4Compressor.INSTANCE);
|
||||
byte[] expectedBytes = new byte[inputLen * 3];
|
||||
ByteBuf expectedBuf = Unpooled.wrappedBuffer(expectedBytes);
|
||||
expectedBuf.writerIndex(0);
|
||||
expectedBuf.writeBytes(uncompressable);
|
||||
expectedBuf.writeBytes(uncompressable);
|
||||
expectedBuf.writeBytes(compressable);
|
||||
|
||||
ByteBuf outbound = transformer.transformOutbound(expectedBuf);
|
||||
ByteBuf inbound = transformer.transformInbound(outbound, FLAGS);
|
||||
|
||||
// reset reader index on expectedBuf back to 0 as it will have been entirely consumed by the transformOutbound() call
|
||||
expectedBuf.readerIndex(0);
|
||||
Assert.assertEquals(expectedBuf, inbound);
|
||||
}
|
||||
|
||||
private void roundTrip(ReusableBuffer input, Compressor compressor, ChecksumType checksum, int blockSize)
|
||||
{
|
||||
ChecksummingTransformer transformer = new ChecksummingTransformer(checksum, blockSize, compressor);
|
||||
ByteBuf expectedBuf = input.toByteBuf();
|
||||
|
||||
ByteBuf outbound = transformer.transformOutbound(expectedBuf);
|
||||
ByteBuf inbound = transformer.transformInbound(outbound, FLAGS);
|
||||
|
||||
// reset reader index on expectedBuf back to 0 as it will have been entirely consumed by the transformOutbound() call
|
||||
expectedBuf.readerIndex(0);
|
||||
Assert.assertEquals(expectedBuf, inbound);
|
||||
}
|
||||
|
||||
private Gen<Pair<ReusableBuffer, Integer>> inputWithCorruptablePosition()
|
||||
{
|
||||
// we only generate corruption for byte 2 onward. This is to skip introducing corruption in the number
|
||||
// of chunks (which isn't checksummed
|
||||
return inputs().flatMap(s -> integers().between(2, s.length + 2).map(i -> Pair.create(s, i)));
|
||||
}
|
||||
|
||||
private static Gen<ReusableBuffer> inputs()
|
||||
{
|
||||
Gen<ReusableBuffer> randomStrings = inputs(0, MAX_INPUT_SIZE, 0, (1 << 8) - 1);
|
||||
Gen<ReusableBuffer> highlyCompressable = inputs(1, MAX_INPUT_SIZE, 'c', 'e');
|
||||
return randomStrings.mix(highlyCompressable, 50);
|
||||
}
|
||||
|
||||
private static Gen<ReusableBuffer> inputs(int minSize, int maxSize, int smallestByte, int largestByte)
|
||||
{
|
||||
ReusableBuffer buffer = new ReusableBuffer(new byte[maxSize]);
|
||||
Constraint byteGen = Constraint.between(smallestByte, largestByte);
|
||||
Constraint lengthGen = Constraint.between(minSize, maxSize);
|
||||
Gen<ReusableBuffer> gen = td -> {
|
||||
int size = (int) td.next(lengthGen);
|
||||
buffer.length = size;
|
||||
for (int i = 0; i < size; i++)
|
||||
buffer.bytes[i] = (byte) td.next(byteGen);
|
||||
return buffer;
|
||||
};
|
||||
return gen;
|
||||
}
|
||||
|
||||
private Gen<ReusableBuffer> zeroLengthInputs()
|
||||
{
|
||||
return arbitrary().constant(new ReusableBuffer(new byte[0]));
|
||||
}
|
||||
|
||||
private Gen<Compressor> compressors()
|
||||
{
|
||||
return arbitrary().pick(null, LZ4Compressor.INSTANCE, SnappyCompressor.INSTANCE);
|
||||
}
|
||||
|
||||
private Gen<ChecksumType> checksumTypes()
|
||||
{
|
||||
return arbitrary().enumValuesWithNoOrder(ChecksumType.class);
|
||||
}
|
||||
|
||||
private Gen<Integer> blocksizes()
|
||||
{
|
||||
return arbitrary().constant(DEFAULT_BLOCK_SIZE);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame.checksum;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.transport.frame.compress.LZ4Compressor;
|
||||
import org.apache.cassandra.utils.ChecksumType;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
|
||||
/**
|
||||
* When we use LZ4 with "fast" functions its unsafe in the case the stream is corrupt; for networking checksuming is
|
||||
* after lz4 decompresses (see CASSANDRA-15299) which means that lz4 can crash the process.
|
||||
*
|
||||
* This test is stand alone for the reason that this test is known to cause the JVM to crash. Given the way we run tests
|
||||
* in CI this will kill the runner which means the file will be marked as failed; if this test was embedded into another
|
||||
* test file then all the other tests would be ignored if this crashes.
|
||||
*/
|
||||
public class ChecksummingWithCorruptedLZ4DoesNotCrashTest
|
||||
{
|
||||
@BeforeClass
|
||||
public static void init()
|
||||
{
|
||||
// required as static ChecksummingTransformer instances read default block size from config
|
||||
DatabaseDescriptor.clientInitialization();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCrash() throws IOException
|
||||
{
|
||||
// We found lz4 caused the JVM to crash, so used the input (bytes and byteToCorrupt) to the test which crashed
|
||||
// to reproduce.
|
||||
// It was found that the same input does not cause lz4 to crash by it self but needed repeated calls with this
|
||||
// input produce such a failure.
|
||||
String failureHex;
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("test/data/CASSANDRA-15313/lz4-jvm-crash-failure.txt"), StandardCharsets.UTF_8))) {
|
||||
failureHex = reader.readLine().trim();
|
||||
}
|
||||
byte[] failure = ByteBufUtil.decodeHexDump(failureHex);
|
||||
ReusableBuffer buffer = new ReusableBuffer(failure);
|
||||
int byteToCorrupt = 52997;
|
||||
// corrupting these values causes the exception.
|
||||
byte[] corruptionValues = new byte[] { 21, 57, 79, (byte) 179 };
|
||||
// 5k was chosen as the largest number of iterations seen needed to crash.
|
||||
for (int i = 0; i < 5_000 ; i++) {
|
||||
for (byte corruptionValue : corruptionValues) {
|
||||
try {
|
||||
ChecksummingTransformerTest.roundTripWithCorruption(Pair.create(buffer, byteToCorrupt), corruptionValue, LZ4Compressor.INSTANCE, ChecksumType.ADLER32);
|
||||
} catch (AssertionError e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.transport.frame.checksum;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
|
||||
/**
|
||||
* Wrapper around byte[] with length which is expected to be reused with different data. It is expected that the bytes
|
||||
* are directly modified and the length gets updated to reflect; this is done to avoid producing unneeded garbage.
|
||||
*
|
||||
* This class is not thread safe.
|
||||
*/
|
||||
public final class ReusableBuffer
|
||||
{
|
||||
public final byte[] bytes;
|
||||
public int length;
|
||||
|
||||
public ReusableBuffer(byte[] bytes)
|
||||
{
|
||||
this.bytes = bytes;
|
||||
this.length = bytes.length;
|
||||
}
|
||||
|
||||
public ByteBuf toByteBuf() {
|
||||
return Unpooled.wrappedBuffer(bytes, 0, length);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return ByteBufferUtil.bytesToHex(ByteBuffer.wrap(bytes, 0, length));
|
||||
}
|
||||
}
|
||||
|
|
@ -24,8 +24,6 @@ package org.apache.cassandra.stress.settings;
|
|||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
||||
import com.datastax.driver.core.Metadata;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.stress.util.JavaDriverClient;
|
||||
import org.apache.cassandra.stress.util.ResultLogger;
|
||||
|
|
@ -88,7 +86,7 @@ public class StressSettings implements Serializable
|
|||
{
|
||||
String currentNode = node.randomNode();
|
||||
SimpleClient client = new SimpleClient(currentNode, port.nativePort);
|
||||
client.connect(false, false);
|
||||
client.connect(false);
|
||||
client.execute("USE \"" + schema.keyspace + "\";", org.apache.cassandra.db.ConsistencyLevel.ONE);
|
||||
return client;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue