This commit is contained in:
Maxim Muzafarov 2026-07-31 14:32:30 +08:00 committed by GitHub
commit cfba53f6a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
176 changed files with 10728 additions and 1224 deletions

View File

@ -43,30 +43,51 @@ if [ -z "$CASSANDRA_CONF" -o -z "$CLASSPATH" ]; then
exit 1
fi
JMX_PORT=""
# Try to parse port from configure_jmx method call, when not commented out
if [ -f "$CASSANDRA_CONF/cassandra-env.sh" ]; then
jmx_method_call=$(grep "^configure_jmx \+[0-9]\+$" "$CASSANDRA_CONF/cassandra-env.sh")
if [ ! "x${jmx_method_call}" = "x" ]; then
JMX_PORT=$(echo "${jmx_method_call}" | tr -s " " | cut -d " " -f2)
# Check if protocol is cql (from environment variable)
PROTOCOL_IS_CQL=""
if [ "x$CASSANDRA_CLI_EXECUTION_PROTOCOL" != "x" ]; then
protocol_lower=$(echo "$CASSANDRA_CLI_EXECUTION_PROTOCOL" | tr '[:upper:]' '[:lower:]')
if [ "$protocol_lower" = "cql" ]; then
PROTOCOL_IS_CQL="true"
fi
fi
# In case JMX_PORT is not set (when configure_jmx in cassandra-env.sh is commented out),
# try to parse it from cassandra.yaml.
if [ "x$JMX_PORT" = "x" ]; then
# Check for cql protocol in system properties to determine if we should parse CQL or JMX port
for arg in "$@"; do
case "$arg" in
-Dcassandra.cli.execution.protocol=cql|-Dcassandra.cli.execution.protocol=CQL)
PROTOCOL_IS_CQL="true"
break
;;
esac
done
# Parse port from config files based on protocol
CONNECTION_PORT=""
if [ "$PROTOCOL_IS_CQL" = "true" ]; then
# For CQL, parse native_transport_management_port from cassandra.yaml
if [ -f "$CASSANDRA_CONF/cassandra.yaml" ]; then
JMX_PORT=$(grep jmx_port "$CASSANDRA_CONF/cassandra.yaml" | cut -d ':' -f 2 | tr -d '[[:space:]]')
CONNECTION_PORT=$(grep native_transport_management_port "$CASSANDRA_CONF/cassandra.yaml" | cut -d ':' -f 2 | tr -d '[[:space:]]')
fi
else
# For JMX, parse port from configure_jmx method call, when not commented out
if [ -f "$CASSANDRA_CONF/cassandra-env.sh" ]; then
jmx_method_call=$(grep "^configure_jmx \+[0-9]\+$" "$CASSANDRA_CONF/cassandra-env.sh")
if [ ! "x${jmx_method_call}" = "x" ]; then
CONNECTION_PORT=$(echo "${jmx_method_call}" | tr -s " " | cut -d " " -f2)
fi
fi
# In case CONNECTION_PORT is not set (when configure_jmx in cassandra-env.sh is commented out),
# try to parse it from cassandra.yaml.
if [ "x$CONNECTION_PORT" = "x" ]; then
if [ -f "$CASSANDRA_CONF/cassandra.yaml" ]; then
CONNECTION_PORT=$(grep jmx_port "$CASSANDRA_CONF/cassandra.yaml" | cut -d ':' -f 2 | tr -d '[[:space:]]')
fi
fi
fi
# If, by any chance, it is not there either, set it to default.
if [ "x$JMX_PORT" = "x" ]; then
JMX_PORT=7199
fi
# JMX Port passed via cmd line args (-p 9999 / --port 9999 / --port=9999)
# Connection Port passed via cmd line args (-p 9999 / --port 9999 / --port=9999)
# should override the value from cassandra-env.sh
ARGS=""
JVM_ARGS=""
@ -76,19 +97,19 @@ do
if [ ! $1 ]; then break; fi
case $1 in
-p)
JMX_PORT=$2
CONNECTION_PORT=$2
shift
;;
--port=*)
JMX_PORT=$(echo $1 | cut -d '=' -f 2)
CONNECTION_PORT=$(echo $1 | cut -d '=' -f 2)
;;
--port)
JMX_PORT=$2
CONNECTION_PORT=$2
shift
;;
--ssl)
if [ -f $SSL_FILE ]
then
then
SSL_ARGS=$(cat $SSL_FILE | tr '\n' ' ')
fi
JVM_ARGS="$JVM_ARGS -Dssl.enable=true $SSL_ARGS"
@ -108,6 +129,14 @@ do
shift
done
if [ "x$CONNECTION_PORT" = "x" ]; then
if [ "$PROTOCOL_IS_CQL" = "true" ]; then
CONNECTION_PORT=11211
else
CONNECTION_PORT=7199
fi
fi
if [ "x$MAX_HEAP_SIZE" = "x" ]; then
MAX_HEAP_SIZE="128m"
fi
@ -123,7 +152,7 @@ CMD=$(echo "$JAVA" $JAVA_AGENT -ea -cp "$CLASSPATH" $JVM_OPTS -Xmx$MAX_HEAP_SIZE
-Dcassandra.logdir="$CASSANDRA_LOG_DIR" \
-Dlogback.configurationFile=logback-tools.xml \
$JVM_ARGS \
org.apache.cassandra.tools.NodeTool -p $JMX_PORT $ARGS)
org.apache.cassandra.tools.NodeTool -p $CONNECTION_PORT $ARGS)
if [ "x$ARCHIVE_COMMAND" != "x" ]
then

View File

@ -1134,6 +1134,57 @@ rpc_address: localhost
# enable or disable keepalive on rpc/native connections
rpc_keepalive: true
# Whether to start the native management transport server.
# The address on which the native management transport is bound can be configured
# using rpc_management_address or rpc_management_interface. If neither is set,
# it defaults to rpc_address (or rpc_interface if that was used).
start_native_transport_management: false
# The address or interface to bind the native management transport server to.
# This allows you to bind management operations to a different network interface
# than regular client connections, providing better security isolation.
#
# Set rpc_management_address OR rpc_management_interface, not both.
#
# If neither rpc_management_address nor rpc_management_interface is set,
# the management transport will use the same address as the regular native
# transport (rpc_address or rpc_interface).
#
# For security reasons, you should not expose this port to the internet.
# Firewall it if needed. Consider binding to a management network interface
# that is not accessible from public networks.
# rpc_management_address: localhost
# Set rpc_management_address OR rpc_management_interface, not both. Interfaces
# must correspond to a single address, IP aliasing is not supported.
#
# This allows you to bind the management transport to a specific network
# interface by name (e.g., eth1, eth0). The IP address will be automatically
# resolved from the interface. This is useful when the interface IP may change
# (e.g., DHCP) or when you want to bind to a specific interface without
# hardcoding the IP address.
# rpc_management_interface: eth1
# If you choose to specify the management interface by name and the interface
# has both an ipv4 and an ipv6 address, you can specify which should be chosen
# using rpc_management_interface_prefer_ipv6. If false the first ipv4 address
# will be used. If true the first ipv6 address will be used. Defaults to false
# preferring ipv4. If there is only one address it will be selected regardless
# of ipv4/ipv6.
# rpc_management_interface_prefer_ipv6: false
# Port for the management CQL native transport to listen for clients on.
# For security reasons, you should not expose this port to the internet.
# Firewall it if needed.
native_transport_management_port: 11211
# The maximum threads for handling management requests are set
# separately from regular requests to allow better isolation and
# prioritization of management operations. This is important to
# ensure that management operations can proceed even under a high
# load of regular client requests. Defaults to 2.
# native_transport_management_max_threads: 2
# Uncomment to set socket buffer size for internode communication
# Note that when setting this, the buffer size is limited by net.core.wmem_max
# and when not setting it it is defined by net.ipv4.tcp_wmem

View File

@ -1119,6 +1119,57 @@ rpc_address: localhost
# enable or disable keepalive on rpc/native connections
rpc_keepalive: true
# Whether to start the native management transport server.
# The address on which the native management transport is bound can be configured
# using rpc_management_address or rpc_management_interface. If neither is set,
# it defaults to rpc_address (or rpc_interface if that was used).
start_native_transport_management: false
# The address or interface to bind the native management transport server to.
# This allows you to bind management operations to a different network interface
# than regular client connections, providing better security isolation.
#
# Set rpc_management_address OR rpc_management_interface, not both.
#
# If neither rpc_management_address nor rpc_management_interface is set,
# the management transport will use the same address as the regular native
# transport (rpc_address or rpc_interface).
#
# For security reasons, you should not expose this port to the internet.
# Firewall it if needed. Consider binding to a management network interface
# that is not accessible from public networks.
# rpc_management_address: localhost
# Set rpc_management_address OR rpc_management_interface, not both. Interfaces
# must correspond to a single address, IP aliasing is not supported.
#
# This allows you to bind the management transport to a specific network
# interface by name (e.g., eth1, eth0). The IP address will be automatically
# resolved from the interface. This is useful when the interface IP may change
# (e.g., DHCP) or when you want to bind to a specific interface without
# hardcoding the IP address.
# rpc_management_interface: eth1
# If you choose to specify the management interface by name and the interface
# has both an ipv4 and an ipv6 address, you can specify which should be chosen
# using rpc_management_interface_prefer_ipv6. If false the first ipv4 address
# will be used. If true the first ipv6 address will be used. Defaults to false
# preferring ipv4. If there is only one address it will be selected regardless
# of ipv4/ipv6.
# rpc_management_interface_prefer_ipv6: false
# Port for the management CQL native transport to listen for clients on.
# For security reasons, you should not expose this port to the internet.
# Firewall it if needed.
native_transport_management_port: 11211
# The maximum threads for handling management requests are set
# separately from regular requests to allow better isolation and
# prioritization of management operations. This is important to
# ensure that management operations can proceed even under a high
# load of regular client requests. Defaults to 2.
# native_transport_management_max_threads: 2
# Uncomment to set socket buffer size for internode communication
# Note that when setting this, the buffer size is limited by net.core.wmem_max
# and when not setting it it is defined by net.ipv4.tcp_wmem

View File

@ -1498,6 +1498,15 @@ Table of Contents
acknowledged the request.
<blockfor> is an [int] representing the number of replicas whose
acknowledgement is required to achieve <cl>.
0x1800 COMMAND_FAILED: An exception occurred during command execution. This error is returned
when a command executed via the native management interface fails during execution.
The exception may or may not include more detail in the accompanying error message.
The rest of the ERROR message body will be
<execution_id>
where:
<execution_id> is a [uuid] representing the unique identifier for the command
execution that failed. This identifier can be used to correlate the
error with command execution logs on the server.
0x2000 Syntax_error: The submitted query has a syntax error.
0x2100 Unauthorized: The logged user doesn't have the right to perform

View File

@ -148,6 +148,8 @@ K_MODIFY: M O D I F Y;
K_AUTHORIZE: A U T H O R I Z E;
K_DESCRIBE: D E S C R I B E;
K_EXECUTE: E X E C U T E;
K_COMMAND: C O M M A N D;
K_INVOKE: I N V O K E;
K_NORECURSIVE: N O R E C U R S I V E;
K_MBEAN: M B E A N;
K_MBEANS: M B E A N S;

View File

@ -294,6 +294,7 @@ cqlStatement returns [CQLStatement.Raw stmt]
| st55=securityLabelOnUserTypeStatement { $stmt = st55; }
| st56=commentOnUserTypeFieldStatement { $stmt = st56; }
| st57=securityLabelOnUserTypeFieldStatement { $stmt = st57; }
| st58=executeCommandStatement { $stmt = st58; }
;
/*
@ -1370,6 +1371,53 @@ securityLabelOnUserTypeFieldStatement returns [SecurityLabelOnUserTypeFieldState
{ $stmt = new SecurityLabelOnUserTypeFieldStatement.Raw($typeFieldRef.typeName, $typeFieldRef.field, label != null ? $label.text : null, provider); }
;
/**
* INVOKE COMMAND <commandName> [WITH key1 = value1 AND key2 = value2];
*/
executeCommandStatement returns [ExecuteCommandStatement.Raw stmt]
@init {
stmtBegins();
java.util.Map<String, Object> args = new java.util.LinkedHashMap<>();
}
: K_INVOKE K_COMMAND cmdName=noncol_ident
(K_WITH commandProperties[args])?
{
$stmt = new ExecuteCommandStatement.Raw(cmdName.toString(), args);
}
;
commandProperties[java.util.Map<String, Object> args]
: commandProperty[args] (K_AND commandProperty[args])*
;
commandProperty[java.util.Map<String, Object> args]
: k=noncol_ident '=' v=commandPropertyValue
{
String key = k.toString();
Object value = v;
if (args.put(key, value) != null)
{
addRecognitionError("Duplicate argument: " + key);
}
}
;
commandPropertyValue returns [Object value]
: s=STRING_LITERAL { $value = $s.text; }
| i=INTEGER { $value = $i.text; }
| f=FLOAT { $value = $f.text; }
| b=BOOLEAN { $value = $b.text; }
| l=commandListValue { $value = l; }
;
commandListValue returns [java.util.List<String> value]
@init { java.util.List<String> l = new java.util.ArrayList<>(); }
: '[' ( s1=STRING_LITERAL { l.add($s1.text); }
( ',' sn=STRING_LITERAL { l.add($sn.text); } )*
)? ']'
{ $value = l; }
;
/**
* DROP TABLE [IF EXISTS] <table>;
*/
@ -1980,7 +2028,7 @@ collectionLiteral returns [Term.Raw value]
listLiteral returns [Term.Raw value]
@init {List<Term.Raw> l = new ArrayList<Term.Raw>();}
@after {$value = new ArrayLiteral(l);}
@after {$value = new ArrayLiteral(l);}
: '[' ( t1=term { l.add(t1); } ( ',' tn=term { l.add(tn); } )* )? ']' { $value = new ArrayLiteral(l); }
;
@ -2483,5 +2531,7 @@ basic_unreserved_keyword returns [String str]
| K_LABELS
| K_FIELD
| K_COLUMN
| K_COMMAND
| K_INVOKE
) { $str = $k.text; }
;

View File

@ -82,7 +82,8 @@ public enum AuditLogEntryType
UNAUTHORIZED_ATTEMPT(AuditLogEntryCategory.AUTH),
LOGIN_SUCCESS(AuditLogEntryCategory.AUTH),
LIST_SUPERUSERS(AuditLogEntryCategory.DCL),
JMX(AuditLogEntryCategory.JMX);
JMX(AuditLogEntryCategory.JMX),
EXECUTE_COMMAND(AuditLogEntryCategory.OTHER);
private final AuditLogEntryCategory category;

View File

@ -28,8 +28,8 @@ import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
*/
public abstract class AbstractCIDRAuthorizer implements ICIDRAuthorizer
{
protected static CIDRPermissionsManager cidrPermissionsManager;
protected static CIDRGroupsMappingManager cidrGroupsMappingManager;
public static CIDRPermissionsManager cidrPermissionsManager;
public static CIDRGroupsMappingManager cidrGroupsMappingManager;
protected static CIDRAuthorizerMetrics cidrAuthorizerMetrics;

View File

@ -45,6 +45,7 @@ import com.google.common.util.concurrent.Uninterruptibles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
import org.apache.cassandra.cache.UnweightedCacheSize;
import org.apache.cassandra.concurrent.ExecutorPlus;
import org.apache.cassandra.concurrent.ScheduledExecutors;
@ -468,6 +469,36 @@ public class AuthCache<K, V> implements AuthCacheMBean, UnweightedCacheSize, Shu
return Ints.checkedCast(getEstimatedSize());
}
/**
* Accepts a visitor for this cache instance. Subclasses should override this method to dispatch
* to the appropriate visitor method based on the specific MBean interface they implement.
* @param visitor the visitor to accept
*/
public void accept(MBeanVisitor visitor)
{
visitor.visit(this);
}
/**
* Visitor interface for processing auth cache MBeans.
* Allows type-safe iteration over different cache types without instanceof checks.
*/
public interface MBeanVisitor
{
/** Visits a credentials cache MBean. */
default void visitCredentials(PasswordAuthenticator.CredentialsCacheMBean cache) {}
/** Visits a JMX permissions cache MBean. */
default void visitJmxPermissions(AuthorizationProxy.JmxPermissionsCacheMBean cache) {}
/** Visits a permissions cache MBean. */
default void visitPermissions(PermissionsCacheMBean cache) {}
/** Visits a network permissions cache MBean. */
default void visitNetwork(NetworkPermissionsCacheMBean cache) {}
/** Visits a roles cache MBean. */
default void visitRoles(RolesCacheMBean cache) {}
/** Visits a generic auth cache (fallback for caches that don't implement specific MBean interfaces).*/
default void visit(AuthCacheMBean cache) {}
}
private class MetricsUpdater implements StatsCounter
{
@Override

View File

@ -26,6 +26,7 @@ import javax.annotation.concurrent.ThreadSafe;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -60,6 +61,11 @@ public class AuthCacheService
}
}
public synchronized Set<AuthCache<?, ?>> getCaches()
{
return Sets.newHashSet(caches);
}
/**
* NOTE: Can only be called once per instance run.
*

View File

@ -52,4 +52,10 @@ public class NetworkPermissionsCache extends AuthCache<RoleResource, DCPermissio
super.unregisterMBean();
MBeanWrapper.instance.unregisterMBean(MBEAN_NAME_BASE + DEPRECATED_CACHE_NAME, MBeanWrapper.OnException.LOG);
}
@Override
public void accept(MBeanVisitor visitor)
{
visitor.visitNetwork(this);
}
}

View File

@ -384,6 +384,12 @@ public class PasswordAuthenticator implements IAuthenticator, AuthCache.BulkLoad
{
invalidate(roleName);
}
@Override
public void accept(MBeanVisitor visitor)
{
visitor.visitCredentials(this);
}
}
public static interface CredentialsCacheMBean extends AuthCacheMBean

View File

@ -50,4 +50,10 @@ public class PermissionsCache extends AuthCache<Pair<AuthenticatedUser, IResourc
{
invalidate(Pair.create(new AuthenticatedUser(roleName), Resources.fromName(resourceName)));
}
@Override
public void accept(MBeanVisitor visitor)
{
visitor.visitPermissions(this);
}
}

View File

@ -82,4 +82,10 @@ public class RolesCache extends AuthCache<RoleResource, Set<Role>> implements Ro
{
invalidate(RoleResource.role(roleName));
}
@Override
public void accept(MBeanVisitor visitor)
{
visitor.visitRoles(this);
}
}

View File

@ -46,6 +46,7 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.auth.AuthCache;
import org.apache.cassandra.auth.AuthCacheMBean;
import org.apache.cassandra.auth.AuthCacheService;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.auth.JMXResource;
import org.apache.cassandra.auth.Permission;
@ -596,6 +597,8 @@ public class AuthorizationProxy implements InvocationHandler
() -> true);
MBeanWrapper.instance.registerMBean(this, MBEAN_NAME_BASE + DEPRECATED_CACHE_NAME);
// Registration makes this cache discovearble by the management transport MBean accessor
AuthCacheService.instance.register(this);
}
public void invalidatePermissions(String roleName)
@ -609,6 +612,12 @@ public class AuthorizationProxy implements InvocationHandler
super.unregisterMBean();
MBeanWrapper.instance.unregisterMBean(MBEAN_NAME_BASE + DEPRECATED_CACHE_NAME, MBeanWrapper.OnException.LOG);
}
@Override
public void accept(MBeanVisitor visitor)
{
visitor.visitJmxPermissions(this);
}
}
public static interface JmxPermissionsCacheMBean extends AuthCacheMBean

View File

@ -36,6 +36,16 @@ public enum CassandraRelevantEnv
JAVA_HOME ("JAVA_HOME"),
CIRCLECI("CIRCLECI"),
CASSANDRA_SKIP_SYNC("CASSANDRA_SKIP_SYNC"),
/**
* Defines the protocol used by the Cassandra CLI to connect to Cassandra nodes.
* Possible values are {@code "static_mbeans"}, {@code "command_mbeans"} or {@code cql}.
* By default, the Cassandra CLI uses the JMX protocol via static MBeans.
*/
CASSANDRA_CLI_EXECUTION_PROTOCOL("CASSANDRA_CLI_EXECUTION_PROTOCOL"),
/** Whether to show the execution id for cli command output, useful for correlating commands with logs and tracing. */
CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID("CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID"),
/** How long the Cassandra CLI waits for a server response when executing a command over CQL, in seconds. */
CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS("CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS"),
/** By default, the standard Cassandra CLI layout is used for backward compatibility, however,
* the new Picocli layout can be enabled by setting this property to the {@code "picocli"}. */
CASSANDRA_CLI_LAYOUT("CASSANDRA_CLI_LAYOUT"),

View File

@ -97,6 +97,16 @@ public enum CassandraRelevantProperties
CACHEABLE_MUTATION_SIZE_LIMIT("cassandra.cacheable_mutation_size_limit_bytes", convertToString(1_000_000)),
CASSANDRA_ALLOW_SIMPLE_STRATEGY("cassandra.allow_simplestrategy"),
CASSANDRA_AVAILABLE_PROCESSORS("cassandra.available_processors"),
/**
* Defines the protocol used by the Cassandra CLI to connect to Cassandra nodes.
* Possible values are {@code "static_mbean"}, {@code "command_mbean"} or {@code cql}.
* By default, the Cassandra CLI uses the JMX protocol via static MBeans.
*/
CASSANDRA_CLI_EXECUTION_PROTOCOL("cassandra.cli.execution.protocol", "static_mbean"),
/** Whether to show the execution id for cli command output, useful for correlating commands with logs and tracing. */
CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID("cassandra.cli.execution.show_execution_id", "false"),
/** How long the Cassandra CLI waits for a server response when executing a command over CQL, in seconds. */
CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS("cassandra.cli.execution.timeout_seconds", "0"),
/** By default, the standard Cassandra CLI layout is used for backward compatibility, however,
* the new Picocli layout can be enabled by setting this property to the {@code "picocli"}. */
CASSANDRA_CLI_LAYOUT("cassandra.cli.layout", "airline"),
@ -419,6 +429,8 @@ public enum CassandraRelevantProperties
MX4JPORT("mx4jport"),
NANOTIMETOMILLIS_TIMESTAMP_UPDATE_INTERVAL("cassandra.NANOTIMETOMILLIS_TIMESTAMP_UPDATE_INTERVAL", "10000"),
NATIVE_EPOLL_ENABLED("cassandra.native.epoll.enabled", "true"),
/** This is the port used with RPC address for the management native protocol to communicate with clients that manage the node. */
NATIVE_TRANSPORT_MANAGEMENT_PORT("cassandra.native_transport_management_port"),
/** This is the port used with RPC address for the native protocol to communicate with clients. Now that thrift RPC is no longer in use there is no RPC port. */
NATIVE_TRANSPORT_PORT("cassandra.native_transport_port"),
NEVER_PURGE_TOMBSTONES("cassandra.never_purge_tombstones"),
@ -580,6 +592,7 @@ public enum CassandraRelevantProperties
SSL_ENABLE("ssl.enable"),
SSL_STORAGE_PORT("cassandra.ssl_storage_port"),
START_GOSSIP("cassandra.start_gossip", "true"),
START_NATIVE_MANAGEMENT_TRANSPORT("cassandra.start_native_management_transport"),
START_NATIVE_TRANSPORT("cassandra.start_native_transport"),
STORAGE_DIR("cassandra.storagedir"),
STORAGE_HOOK("cassandra.storage_hook"),

View File

@ -370,6 +370,20 @@ public class Config
@Replaces(oldName = "native_transport_receive_queue_capacity_in_bytes", converter = Converters.BYTES_DATASTORAGE, deprecated = true)
public DataStorageSpec.IntBytesBound native_transport_receive_queue_capacity = new DataStorageSpec.IntBytesBound("1MiB");
/**
* Management RPC address and interface refer to the address/interface used for the native management protocol
* to communicate with management clients. If not explicitly configured, these default to the regular RPC address
* configuration (rpc_address or rpc_interface).
* <p>
* native_transport_management_port is the port paired with the management RPC address to bind on.
*/
public String rpc_management_address;
public String rpc_management_interface;
public boolean rpc_management_interface_prefer_ipv6 = false;
public boolean start_native_transport_management = false;
public int native_transport_management_port = 11211;
public int native_transport_management_max_threads = 2;
/**
* Max size of values in SSTables, in MebiBytes.
* Default is the same as the native protocol frame limit: 256MiB.

View File

@ -148,6 +148,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.CONFIG_LOA
import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_STCS_IN_L0;
import static org.apache.cassandra.config.CassandraRelevantProperties.INITIAL_TOKEN;
import static org.apache.cassandra.config.CassandraRelevantProperties.IO_NETTY_TRANSPORT_ESTIMATE_SIZE_ON_SUBMIT;
import static org.apache.cassandra.config.CassandraRelevantProperties.NATIVE_TRANSPORT_MANAGEMENT_PORT;
import static org.apache.cassandra.config.CassandraRelevantProperties.NATIVE_TRANSPORT_PORT;
import static org.apache.cassandra.config.CassandraRelevantProperties.OS_ARCH;
import static org.apache.cassandra.config.CassandraRelevantProperties.PARTITIONER;
@ -216,6 +217,7 @@ public class DatabaseDescriptor
private static InetAddress broadcastAddress;
private static InetAddress rpcAddress;
private static InetAddress broadcastRpcAddress;
private static InetAddress rpcManagementAddress;
private static SeedProvider seedProvider;
private static IInternodeAuthenticator internodeAuthenticator = new AllowAllInternodeAuthenticator();
@ -1494,6 +1496,7 @@ public class DatabaseDescriptor
rpcAddress = null;
broadcastAddress = null;
broadcastRpcAddress = null;
rpcManagementAddress = null;
/* Local IP, hostname or interface to bind services to */
if (config.listen_address != null && config.listen_interface != null)
@ -1581,6 +1584,32 @@ public class DatabaseDescriptor
throw new ConfigurationException("If rpc_address is set to a wildcard address (" + config.rpc_address + "), then " +
"you must set broadcast_rpc_address to a value other than " + config.rpc_address, false);
}
/* Local IP, hostname or interface to bind Management RPC server to */
if (config.rpc_management_address != null && config.rpc_management_interface != null)
{
throw new ConfigurationException("Set rpc_management_address OR rpc_management_interface, not both", false);
}
else if (config.rpc_management_address != null)
{
try
{
rpcManagementAddress = InetAddress.getByName(config.rpc_management_address);
}
catch (UnknownHostException e)
{
throw new ConfigurationException("Unknown host in rpc_management_address " + config.rpc_management_address, false);
}
}
else if (config.rpc_management_interface != null)
{
rpcManagementAddress = getNetworkInterfaceAddress(config.rpc_management_interface, "rpc_management_interface", config.rpc_management_interface_prefer_ipv6);
}
else
{
// Default to regular rpc_address if not specified
rpcManagementAddress = rpcAddress;
}
}
public static void applyEncryptionContext()
@ -3606,6 +3635,18 @@ public class DatabaseDescriptor
return rpcAddress;
}
/**
* This is the address used to bind for the native management protocol to communicate with management clients.
* If not explicitly configured via rpc_management_address or rpc_management_interface, defaults to the regular
* rpc_address. The address alone is not enough to uniquely identify this instance because multiple instances
* might use the same interface with different ports.
*/
public static InetAddress getRpcManagementAddress()
{
assert rpcManagementAddress != null;
return rpcManagementAddress;
}
public static void setBroadcastRpcAddress(InetAddress broadcastRPCAddr)
{
broadcastRpcAddress = broadcastRPCAddr;
@ -3850,6 +3891,38 @@ public class DatabaseDescriptor
);
}
public static boolean startNativeTransportManagement()
{
return conf.start_native_transport_management;
}
@VisibleForTesting
public static void setStartNativeTransportManagement(boolean start)
{
conf.start_native_transport_management = start;
}
public static int getNativeTransportManagementPort()
{
return NATIVE_TRANSPORT_MANAGEMENT_PORT.getInt(conf.native_transport_management_port);
}
@VisibleForTesting
public static void setNativeTransportPortManagement(int port)
{
conf.native_transport_management_port = port;
}
public static int getNativeTransportManagementMaxThreads()
{
return conf.native_transport_management_max_threads;
}
public static void setNativeTransportManagementMaxThreads(int max_threads)
{
conf.native_transport_management_max_threads = max_threads;
}
public static Config.PaxosVariant getPaxosVariant()
{
return conf.paxos_variant;

View File

@ -20,12 +20,14 @@ package org.apache.cassandra.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@ -49,7 +51,6 @@ import org.apache.cassandra.service.disk.usage.DiskUsageMonitor;
import org.apache.cassandra.utils.LocalizeString;
import static java.lang.String.format;
import static java.util.stream.Collectors.toSet;
/**
* Configuration settings for guardrails populated from the Yaml file.
@ -1581,12 +1582,24 @@ public class GuardrailsOptions implements GuardrailsConfig
"than the fail threshold %s", warn, name, fail));
}
/**
* See {@code GuardrailsConfigCommandTest}, that relies on the fact that the error messages for invalid properties
* are deterministic and contain the list of invalid properties in the same order as they were provided in the command.
*
* @param properties set of properties to validate.
* @param name name of the property for error messages.
* @return set of properties converted to lower case.
*/
private static Set<String> validateTableProperties(Set<String> properties, String name)
{
if (properties == null)
throw new IllegalArgumentException(format("Invalid value for %s: null is not allowed", name));
Set<String> lowerCaseProperties = properties.stream().map(String::toLowerCase).collect(toSet());
// We use LinkedHashSet to preserve the order of properties in error messages. This is
// used to avoid confusion in an error message when the CQL/nodetool commands are invoked
// with properties in a specific order.
Set<String> lowerCaseProperties = properties.stream().map(LocalizeString::toLowerCaseLocalized)
.collect(Collectors.toCollection(LinkedHashSet::new));
Set<String> diff = Sets.difference(lowerCaseProperties, TableAttributes.allKeywords());
@ -1601,7 +1614,11 @@ public class GuardrailsOptions implements GuardrailsConfig
if (properties == null)
throw new IllegalArgumentException(format("Invalid value for %s: null is not allowed", name));
Set<String> lowerCaseProperties = properties.stream().map(LocalizeString::toLowerCaseLocalized).collect(toSet());
// We use LinkedHashSet to preserve the order of properties in error messages. This is
// used to avoid confusion in an error message when the CQL/nodetool commands are invoked
// with properties in a specific order.
Set<String> lowerCaseProperties = properties.stream().map(LocalizeString::toLowerCaseLocalized)
.collect(Collectors.toCollection(LinkedHashSet::new));
for (String requiredKeyword : KeyspaceAttributes.requiredKeywords())
{

View File

@ -0,0 +1,168 @@
/*
* 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.cql3.statements;
import java.util.List;
import java.util.Map;
import org.apache.cassandra.audit.AuditLogContext;
import org.apache.cassandra.audit.AuditLogEntryType;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQLStatement;
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.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.UUIDType;
import org.apache.cassandra.exceptions.CommandRequestExecutionException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.management.CommandAuthorizationException;
import org.apache.cassandra.management.CommandExecutionArgsSerde;
import org.apache.cassandra.management.CommandExecutionException;
import org.apache.cassandra.management.CommandInvokerService;
import org.apache.cassandra.management.CommandValidationException;
import org.apache.cassandra.management.api.Command;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.transport.messages.ResultMessage;
import static org.apache.cassandra.management.ManagementUtils.causeMessages;
import static org.apache.cassandra.management.ManagementUtils.findRegistryCommand;
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
public class ExecuteCommandStatement
{
public static final String COMMAND_RESULT_SCHEMA_EXECUTION_ID = "execution_id";
public static final String COMMAND_RESULT_SCHEMA_OUTPUT = "output";
public static class Raw extends CQLStatement.Raw implements CQLStatement
{
private final String commandName;
private final Map<String, Object> args;
public Raw(String commandName, Map<String, Object> args)
{
this.commandName = commandName;
this.args = args;
}
public String commandName()
{
return commandName;
}
public Map<String, Object> args()
{
return args;
}
public CQLStatement prepare(ClientState state)
{
return this;
}
public void authorize(ClientState state) throws UnauthorizedException
{
// TODO: CASSANDRA-XXXXX Restrict command execution to deployments without authentication
// This is a temporary limitation until full authentication support is implemented.
if (DatabaseDescriptor.getAuthenticator().requireAuthentication())
{
throw new UnauthorizedException("Command execution via management port is currently only supported " +
"when authentication is disabled (AllowAllAuthenticator). " +
"Full authentication and authorization support will be added in a " +
"future release.");
}
// Validate login (will succeed with AllowAllAuthenticator)
state.validateLogin();
}
@Override
public void validate(ClientState state) throws InvalidRequestException
{
Command<?> command = findRegistryCommand(commandName, CommandInvokerService.instance.getRegistry());
if (command == null)
throw new InvalidRequestException("Command not found: " + commandName);
}
@Override
public ResultMessage execute(QueryState state, QueryOptions options, Dispatcher.RequestTime requestTime)
{
try
{
ClientState clientState = state.getClientState();
if (!clientState.isInternal && !clientState.isManagement())
throw new InvalidRequestException("Command execution is only allowed via native management interface");
Command<?> command = findRegistryCommand(commandName, CommandInvokerService.instance.getRegistry());
if (command == null)
throw new InvalidRequestException("Command not found: " + commandName);
CommandInvokerService.CommandResult result = CommandInvokerService.instance
.invokeCommand(commandName,
() -> CommandExecutionArgsSerde.fromMap(args, command.metadata()));
ResultSet resultSet = getCommandResultSet();
resultSet.addColumnValue(bytes(result.getExecutionId()));
resultSet.addColumnValue(bytes(result.getOutput()));
return new ResultMessage.Rows(resultSet);
}
catch (CommandAuthorizationException e)
{
throw new UnauthorizedException(e.getMessage());
}
catch (CommandValidationException e)
{
throw new InvalidRequestException(causeMessages(e), e.getCause());
}
catch (CommandExecutionException e)
{
throw new CommandRequestExecutionException(e.getExecutionId(),
causeMessages(e),
e.getCause());
}
}
private static ResultSet getCommandResultSet()
{
ColumnSpecification executionIdColumn = new ColumnSpecification("system",
"command_output",
new ColumnIdentifier(COMMAND_RESULT_SCHEMA_EXECUTION_ID, true),
UUIDType.instance);
ColumnSpecification outputColumn = new ColumnSpecification("system",
"command_output",
new ColumnIdentifier(COMMAND_RESULT_SCHEMA_OUTPUT, true),
UTF8Type.instance);
return new ResultSet(new ResultSet.ResultMetadata(List.of(executionIdColumn, outputColumn)));
}
public ResultMessage executeLocally(QueryState state, QueryOptions options) throws InvalidRequestException
{
return execute(state, options, Dispatcher.RequestTime.forImmediateExecution());
}
@Override
public AuditLogContext getAuditLogContext()
{
return new AuditLogContext(AuditLogEntryType.EXECUTE_COMMAND, commandName);
}
}
}

View File

@ -177,7 +177,8 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
// Validate table supports dictionary compression
if (!isEnabled)
{
throw new UnsupportedOperationException("Table " + keyspaceName + '.' + tableName + " does not support dictionary compression");
throw new IllegalStateException(format("The compression on table %s.%s is not enabled or SSTable compressor is not a dictionary compressor.",
keyspaceName, tableName));
}
// resolve training config and fail fast when invalid, so we do not reach logic which would e.g. flush unnecessarily.
@ -353,11 +354,11 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
if (lastTraining.isAfter(now.minus(config.minTrainingFrequency, ChronoUnit.MINUTES)))
{
Instant nextEarliestTraining = lastTraining.plus(config.minTrainingFrequency, ChronoUnit.MINUTES);
throw new IllegalArgumentException(format("The next training or importing can occur only at least after %s from the last training which happened at %s. " +
"You can train again no earlier than at %s.",
new DurationSpec.IntMinutesBound(config.minTrainingFrequency, TimeUnit.MINUTES),
lastTraining,
nextEarliestTraining));
throw new RuntimeException(format("The next training or importing can occur only at least after %s from the last training which happened at %s. " +
"You can train again no earlier than at %s.",
new DurationSpec.IntMinutesBound(config.minTrainingFrequency, TimeUnit.MINUTES),
lastTraining,
nextEarliestTraining));
}
}
}

View File

@ -56,7 +56,7 @@ public class CIDRFilteringMetricsTable implements CIDRFilteringMetricsTableMBean
{
public static final String MBEAN_NAME = "org.apache.cassandra.db:type=CIDRFilteringMetricsTable";
private static final CIDRFilteringMetricsTable instance = new CIDRFilteringMetricsTable();
public static final CIDRFilteringMetricsTable instance = new CIDRFilteringMetricsTable();
CIDRFilteringMetricsTable()
{

View File

@ -0,0 +1,43 @@
/*
* 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.exceptions;
import java.util.UUID;
public class CommandRequestExecutionException extends RequestExecutionException
{
private final UUID commandExecutionId;
public CommandRequestExecutionException(UUID commandExecutionId, String msg, Throwable cause)
{
super(ExceptionCode.COMMAND_FAILED, msg, cause);
this.commandExecutionId = commandExecutionId;
}
public CommandRequestExecutionException(UUID commandExecutionId, String msg)
{
super(ExceptionCode.COMMAND_FAILED, msg);
this.commandExecutionId = commandExecutionId;
}
public UUID getCommandExecutionId()
{
return commandExecutionId;
}
}

View File

@ -48,6 +48,7 @@ public enum ExceptionCode
WRITE_FAILURE (0x1500),
CDC_WRITE_FAILURE (0x1600),
CAS_WRITE_UNKNOWN (0x1700),
COMMAND_FAILED (0x1800),
// 2xx: problem validating the request
SYNTAX_ERROR (0x2000),

View File

@ -0,0 +1,141 @@
/*
* 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.management;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.management.api.Command;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.api.CommandExecutionContext;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.management.api.CommandRegistry;
import org.apache.cassandra.management.api.CommandsProvider;
import org.apache.cassandra.management.api.OptionMetadata;
import org.apache.cassandra.management.api.ParameterMetadata;
import static org.apache.cassandra.management.ManagementUtils.loadService;
public class CassandraCommandRegistry implements CommandRegistry
{
private static final Logger logger = LoggerFactory.getLogger(CassandraCommandRegistry.class);
// TODO CASSANDRA-XXXXX These long-running commands block the calling thread for
// the entire command duration. They are excluded from the management API until a
// ProgressCommand (extends Command) interface is implemented to support long-running
// command execution with progress reporting.
static final Set<String> UNSUPPORTED_COMMANDS = Set.of("repair", "consensus_admin");
private final Map<String, Command<?>> commandMap = new ConcurrentHashMap<>();
public CassandraCommandRegistry()
{
for (CommandsProvider provider : loadService(CommandsProvider.class))
provider.commands().forEach(this::register);
}
public void register(Command<?> command)
{
if (UNSUPPORTED_COMMANDS.contains(command.name()))
{
logger.info("Skipping unsupported command '{}': long-running commands require " +
"ProgressCommand support (not yet implemented)", command.name());
return;
}
Command<?> prev = commandMap.putIfAbsent(command.name(), command);
if (prev != null)
throw new IllegalStateException(String.format("Command name conflict: '%s' is already registered",
command.name()));
}
@Override
@SuppressWarnings("unchecked")
public <T> Command<T> command(String name)
{
return (Command<T>) commandMap.get(name);
}
@Override
public Iterable<Map.Entry<String, Command<?>>> commands()
{
return commandMap.entrySet();
}
@Override
public CommandMetadata metadata()
{
// Return metadata that reflects this is a registry, not a leaf command
return new RootCommandMetadata();
}
@Override
public String execute(CommandExecutionArgs arguments, CommandExecutionContext context)
{
// CommandRegistry is not directly executable: routing happens at the invoker/CQL layer.
throw new UnsupportedOperationException(
String.format("CommandRegistry '%s' is not directly executable. " +
"Specify a subcommand. Available commands: %s",
name(),
String.join(", ", commandMap.keySet())));
}
private class RootCommandMetadata implements CommandMetadata
{
@Override
public String name()
{
return "root";
}
@Override
public String description()
{
return "Root command registry - use a specific subcommand";
}
@Override
public List<OptionMetadata> options()
{
return Collections.emptyList();
}
@Override
public List<ParameterMetadata> parameters()
{
return Collections.emptyList();
}
@Override
public List<CommandMetadata> subcommands()
{
return commandMap.values().stream()
.map(Command::metadata)
.collect(Collectors.toList());
}
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.management;
public class CommandAuthorizationException extends Exception
{
public CommandAuthorizationException(String msg)
{
super(msg);
}
}

View File

@ -0,0 +1,252 @@
/*
* 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.management;
import java.lang.reflect.Array;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.cassandra.management.api.ArgumentMetadata;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.management.api.OptionMetadata;
import org.apache.cassandra.management.api.ParameterMetadata;
import org.apache.cassandra.management.picocli.PicocliCommandMetadata;
import org.apache.cassandra.management.picocli.PicocliOptionMetadata;
import org.apache.cassandra.management.picocli.PicocliParameterMetadata;
import org.apache.cassandra.management.picocli.TypeConverterRegistry;
import org.apache.cassandra.utils.JsonUtils;
import static org.apache.cassandra.management.ManagementUtils.normalizeOptionName;
import static org.apache.cassandra.management.api.ParameterMetadata.COMMAND_POSITIONAL_PARAM_PREFIX;
public class CommandExecutionArgsSerde
{
public static String toJson(CommandExecutionArgs args)
{
Map<String, Object> params = new LinkedHashMap<>();
for (Map.Entry<OptionMetadata, Object> entry : args.options().entrySet())
{
OptionMetadata optionMetadata = entry.getKey();
Object value = entry.getValue();
if (value == null)
continue;
String primaryName = normalizeOptionName(optionMetadata.paramLabel());
params.put(primaryName, normalizeJsonValue(value));
}
for (Map.Entry<ParameterMetadata, Object> entry : args.parameters().entrySet())
{
ParameterMetadata paramMetadata = entry.getKey();
Object value = entry.getValue();
if (value == null)
continue;
int index = paramMetadata.index();
String indexKey = COMMAND_POSITIONAL_PARAM_PREFIX + index;
params.put(indexKey, normalizeJsonValue(value));
}
return JsonUtils.writeAsJsonString(params);
}
public static CommandExecutionArgs fromJson(String json, CommandMetadata metadata)
{
if (!(metadata instanceof PicocliCommandMetadata))
throw new IllegalArgumentException("CommandMetadata must be PicocliCommandMetadata for picocli commands");
Map<String, Object> jsonMap = JsonUtils.fromJsonMap(json);
Map<OptionMetadata, Object> options = new LinkedHashMap<>();
Map<ParameterMetadata, Object> parameters = new LinkedHashMap<>();
convertFromMap(jsonMap, metadata, options, parameters);
return new SimpleCommandExecutionArgs(options, parameters);
}
public static CommandExecutionArgs fromMap(Map<String, Object> format, CommandMetadata metadata)
{
if (!(metadata instanceof PicocliCommandMetadata))
throw new IllegalArgumentException("CommandMetadata must be PicocliCommandMetadata for picocli commands");
Map<OptionMetadata, Object> options = new LinkedHashMap<>();
Map<ParameterMetadata, Object> parameters = new LinkedHashMap<>();
convertFromMap(format, metadata, options, parameters);
return new SimpleCommandExecutionArgs(options, parameters);
}
private static void convertFromMap(Map<String, Object> source,
CommandMetadata metadata,
Map<OptionMetadata, Object> options,
Map<ParameterMetadata, Object> parameters)
{
for (Map.Entry<String, Object> entry : source.entrySet())
{
String paramName = entry.getKey();
Object value = entry.getValue();
if (value == null)
continue;
OptionMetadata option = findOptionByNameIgnoreCase(metadata, paramName);
if (option != null)
{
Object convertedValue = convertValue(value, option);
options.put(option, convertedValue);
continue;
}
ParameterMetadata param = findParameterByName(metadata, paramName);
if (param != null)
{
Object convertedValue = convertValue(value, param);
parameters.put(param, convertedValue);
}
else
{
throw new IllegalArgumentException("Unknown parameter: " + paramName);
}
}
}
/** Convert value using custom converter if provided, otherwise use basic conversion. */
private static Object convertValue(Object value, ArgumentMetadata argSpec)
{
try
{
// Custom type converters are only supported for picocli-based metadata.
if (argSpec instanceof PicocliParameterMetadata)
return ((PicocliParameterMetadata) argSpec).convertValue(value);
else if (argSpec instanceof PicocliOptionMetadata)
return ((PicocliOptionMetadata) argSpec).convertValue(value);
return TypeConverterRegistry.convertValueBasic(value, argSpec.type());
}
catch (IllegalArgumentException e)
{
throw e;
}
catch (Exception e)
{
throw new IllegalArgumentException(String.format("Failed to convert value '%s' to type %s: %s",
value,
argSpec.type().getName(),
e.getMessage()), e);
}
}
/**
* This method converts values from CommandExecutionArgs into JSON-serializable formats before
* they're put into a Map(String, Object) that will be serialized to JSON. Different implementations
* (e.g., Set, LinkedHashSet) should be normalized to List for consistent JSON output.
* <p>
* The conversion rules are:
* - Converts primitive arrays (String[], int[], etc.) -> Object[] (JSON-serializable)
* - Converts any Collection -> List (JSON-serializable)
* - Leaves other types unchanged
*
* @param value the value to convert.
* @return the converted value.
*/
private static Object normalizeJsonValue(Object value)
{
if (value == null)
return null;
if (value.getClass().isArray())
{
int length = Array.getLength(value);
Object[] array = new Object[length];
for (int i = 0; i < length; i++)
array[i] = normalizeJsonValue(Array.get(value, i));
return array;
}
if (value instanceof java.util.Collection)
{
return ((java.util.Collection<?>) value).stream()
.map(CommandExecutionArgsSerde::normalizeJsonValue)
.collect(Collectors.toList());
}
return value;
}
/**
* Find ParameterMetadata by name (supports both an index format and paramLabel).
*/
private static ParameterMetadata findParameterByName(CommandMetadata metadata, String name)
{
if (name.startsWith(COMMAND_POSITIONAL_PARAM_PREFIX))
{
try
{
int index = Integer.parseInt(name.substring(COMMAND_POSITIONAL_PARAM_PREFIX.length()));
for (ParameterMetadata param : metadata.parameters())
{
if (param.index() == index)
return param;
}
}
catch (NumberFormatException e)
{
// Not a valid index format, continue to check paramLabel
}
}
for (ParameterMetadata param : metadata.parameters())
{
String paramLabel = param.paramLabel();
if (paramLabel != null && paramLabel.equalsIgnoreCase(name))
return param;
}
return null;
}
/**
* Find OptionMetadata by name (case-insensitive, normalized).
* Matches against paramLabel and all names/aliases.
*/
private static OptionMetadata findOptionByNameIgnoreCase(CommandMetadata metadata, String name)
{
String normalizedName = normalizeOptionName(name);
for (OptionMetadata option : metadata.options())
{
String paramLabel = normalizeOptionName(option.paramLabel());
if (paramLabel.equalsIgnoreCase(normalizedName))
return option;
for (String alias : option.names())
{
String normalizedAlias = normalizeOptionName(alias);
if (normalizedAlias.equalsIgnoreCase(normalizedName))
return option;
}
}
return null;
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.management;
import java.util.UUID;
public class CommandExecutionException extends Exception
{
private final UUID executionId;
public CommandExecutionException(String message, Throwable cause, UUID executionId)
{
super(message, cause);
this.executionId = executionId;
}
public UUID getExecutionId()
{
return executionId;
}
}

View File

@ -0,0 +1,508 @@
/*
* 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.management;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
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.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import javax.management.ObjectName;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.management.api.Command;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.api.CommandExecutionContext;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.management.api.CommandRegistry;
import org.apache.cassandra.management.api.OptionMetadata;
import org.apache.cassandra.management.api.ParameterMetadata;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.Output;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.MBeanWrapper;
import static java.lang.String.format;
import static org.apache.cassandra.management.ManagementUtils.countCommands;
import static org.apache.cassandra.management.ManagementUtils.findRegistryCommand;
import static org.apache.cassandra.management.ManagementUtils.fullCommandName;
/**
* Service that manages the command registry lifecycle and execution.
* <p>
* Similar to StorageService, SnapshotManager the service coordinates various aspects of command management:
* - Manages lifecycle (initialization, shutdown)
* - Coordinates MBean registration
* - Provides command execution coordination
* - Integrates with daemon startup
* - Manages state (registry, MBean instances)
*/
public class CommandInvokerService implements CommandInvokerServiceMBean
{
private static final String MBEAN_DOMAIN = "org.apache.cassandra.management";
private static final String MBEAN_TYPE_COMMAND = "Command";
private static final int MAX_EXECUTION_HISTORY = 100;
private static final Logger logger = LoggerFactory.getLogger(CommandInvokerService.class);
public static final CommandInvokerService instance = new CommandInvokerService();
private final BoundedExecutionHistory executionHistory = new BoundedExecutionHistory(MAX_EXECUTION_HISTORY);
private final Map<String, ObjectName> commandMBeanNames = new ConcurrentHashMap<>();
private final MBeanAccessor accessor = new InternalNodeMBeanAccessor();
private final CommandRegistry registry;
private volatile boolean started = false;
private CommandInvokerService()
{
this.registry = new CassandraCommandRegistry();
}
public synchronized void start()
{
if (started)
return;
logger.info("Starting command service");
registerCommandMBeansRecursively(registry, "");
MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
started = true;
logger.info("Command service started with '{}' commands", countCommands(registry));
}
public static void shutdown()
{
instance.stop();
}
public synchronized void stop()
{
if (!started)
return;
logger.info("Stopping command service");
unregisterCommandMBeans();
try
{
MBeanWrapper.instance.unregisterMBean(MBEAN_NAME);
}
catch (Exception e)
{
logger.warn("Failed to unregister CommandInvokerService MBean", e);
}
started = false;
logger.info("Command service stopped");
}
public CommandRegistry getRegistry()
{
return registry;
}
/**
* @param fullCommandName Full command name, including command demimiter for subcommands.
* @param argumentsSupplier Supplier of command execution arguments.
* This is used to defer argument construction until after all validation checks are passed.
* @return captured output of the command execution.
*/
public CommandResult invokeCommand(String fullCommandName, Supplier<CommandExecutionArgs> argumentsSupplier)
throws CommandExecutionException, CommandValidationException, CommandAuthorizationException
{
if (!started)
throw new IllegalStateException("CommandInvokerService is not started");
Command<?> command = findRegistryCommand(fullCommandName, registry);
if (command == null)
throw new IllegalArgumentException("Command not found: " + fullCommandName);
return invokeCommand(fullCommandName, command, argumentsSupplier);
}
/**
* Execute a command by name with the given arguments.
*
* @param command to execute.
* @param argumentsSupplier supplier of command execution arguments.
* @return captured output of the command execution.
*/
private CommandResult invokeCommand(String fullCommandName, Command<?> command, Supplier<CommandExecutionArgs> argumentsSupplier)
throws CommandExecutionException, CommandValidationException, CommandAuthorizationException
{
if (!started)
throw new IllegalStateException("CommandInvokerService is not started");
UUID executionId = UUID.randomUUID();
CapturingOutput captured = new CapturingOutput();
ExecutionHistory record = new ExecutionHistory(executionId, fullCommandName, Clock.Global.currentTimeMillis());
CommandExecutionContext executionContext = new ServerCommandExecutionContext(new NodeProbe(accessor, captured.createOutput()));
executionHistory.add(record);
try
{
// TODO: CASSANDRA-XXXXX. Restrict command execution to environments without authentication
// This is a temporary limitation until full authentication support is implemented.
if (DatabaseDescriptor.getAuthenticator().requireAuthentication())
{
throw new CommandAuthorizationException(format("Command execution '%s' via management port is currently " +
"only supported when authentication is disabled " +
"(AllowAllAuthenticator). Full authentication and authorization " +
"support will be added in a future release.", fullCommandName));
}
logger.info("Executing command '{}' with execution ID: {}", fullCommandName, executionId);
CommandExecutionArgs arguments = argumentsSupplier.get();
validateArguments(arguments, command.metadata());
// Currently, for picocli-based commands in C*, which have no structured result,
// the output is written to the Output in the context.
Object ignore = command.execute(arguments, executionContext);
record.completed(Clock.Global.currentTimeMillis());
logger.info("Command '{}' (execution ID: {}) completed successfully", fullCommandName, executionId);
return new CommandResult(executionId,
captured.getCapturedOutput(),
record.startTime,
record.endTime - record.startTime);
}
catch (CommandAuthorizationException e)
{
record.failed(Clock.Global.currentTimeMillis(), e);
logger.error("Command '{}' (execution ID: {}) authorization failed", fullCommandName, executionId, e);
throw e;
}
catch (IllegalStateException | IllegalArgumentException | MarshalException e)
{
record.failed(Clock.Global.currentTimeMillis(), e);
String msg = format("Bad usage for command '%s' (execution ID: %s)", fullCommandName, executionId);
logger.error(msg, e);
throw new CommandValidationException(msg, e);
}
catch (Exception e)
{
record.failed(Clock.Global.currentTimeMillis(), e);
String msg = format("Command '%s' (execution ID: %s) execution failed", fullCommandName, executionId);
logger.error(msg, e);
throw new CommandExecutionException(msg, e, executionId);
}
catch (Throwable e)
{
record.failed(Clock.Global.currentTimeMillis(), e);
logger.error("Command '{}' (execution ID: {}) unexpected error", fullCommandName, executionId, e);
throw new CommandExecutionException(format("Unexpected error while executing '%s': %s",
fullCommandName, e.getMessage()),
e,
executionId);
}
}
@Override
public String[] getCommandNames()
{
List<String> commandNames = new ArrayList<>();
collectCommandNamesRecursively(registry, "", commandNames);
return commandNames.toArray(new String[0]);
}
@VisibleForTesting
List<ExecutionHistory> executionHistory()
{
return executionHistory.snapshot();
}
/** Validate command arguments against metadata. */
protected void validateArguments(CommandExecutionArgs arguments, CommandMetadata metadata)
{
for (OptionMetadata option : metadata.options())
{
if (option.required() && !arguments.hasOption(option))
throw new IllegalArgumentException(String.format("Required option '%s' is missing", option.paramLabel()));
}
for (ParameterMetadata param : metadata.parameters())
{
if (param.required() && !arguments.hasParameter(param))
throw new IllegalArgumentException(String.format("Required parameter at index %d ('%s') is missing",
param.index(), param.paramLabel()));
}
}
private void collectCommandNamesRecursively(CommandRegistry registry,
String parentCommandName,
List<String> result)
{
for (Map.Entry<String, Command<?>> entry : registry.commands())
{
String commandName = entry.getKey();
Command<?> command = entry.getValue();
String fullCommandName = fullCommandName(parentCommandName, commandName);
if (command instanceof CommandRegistry)
collectCommandNamesRecursively((CommandRegistry) command, fullCommandName, result);
else
result.add(fullCommandName);
}
}
@Override
public int getCommandCount()
{
return countCommands(registry);
}
@Override
public String getCommandMBeanName(String fullCommandName)
{
ObjectName objectName = commandMBeanNames.get(fullCommandName);
if (objectName == null)
throw new IllegalArgumentException("Command MBean name not found: " + fullCommandName);
return objectName.toString();
}
public boolean isStarted()
{
return started;
}
private void registerCommandMBeansRecursively(CommandRegistry registry, String parentCommandName)
{
for (Map.Entry<String, Command<?>> e : registry.commands())
{
String commandName = e.getKey();
Command<?> command = e.getValue();
String fullCommandName = fullCommandName(parentCommandName, commandName);
if (command instanceof CommandRegistry)
{
registerCommandMBeansRecursively((CommandRegistry) command, fullCommandName);
}
else
{
try
{
String escapedName = ObjectName.quote(fullCommandName);
ObjectName objectName = new ObjectName(format("%s:type=%s,name=%s",
MBEAN_DOMAIN, MBEAN_TYPE_COMMAND, escapedName));
CommandMBeanAdapter commandMBean = new CommandMBeanAdapter(fullCommandName, command, this::invokeCommand);
MBeanWrapper.instance.registerMBean(commandMBean, objectName, MBeanWrapper.OnException.LOG);
ObjectName prev = commandMBeanNames.putIfAbsent(fullCommandName, objectName);
if (prev != null)
{
throw new IllegalStateException("Command name conflict during MBean registration: '" +
fullCommandName + "' is already registered");
}
logger.debug("Registered command MBean: {} -> {}", fullCommandName, objectName);
}
catch (Exception ex)
{
logger.warn("Failed to register MBean for command: {}", fullCommandName, ex);
}
}
}
}
private void unregisterCommandMBeans()
{
for (ObjectName objectName : commandMBeanNames.values())
MBeanWrapper.instance.unregisterMBean(objectName, MBeanWrapper.OnException.LOG);
commandMBeanNames.clear();
}
@VisibleForTesting
static class CapturingOutput
{
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private final PrintStream output = new PrintStream(buffer, true, StandardCharsets.UTF_8);
private final PrintStream error = new PrintStream(buffer, true, StandardCharsets.UTF_8);
public String getCapturedOutput()
{
output.flush();
error.flush();
return buffer.toString(StandardCharsets.UTF_8);
}
Output createOutput()
{
return new Output(output, error);
}
}
private static class ServerCommandExecutionContext implements CommandExecutionContext
{
private final Map<Class<?>, Object> services = new HashMap<>();
public ServerCommandExecutionContext(NodeProbe probe)
{
services.put(NodeProbe.class, probe);
services.put(Output.class, probe.output());
}
@Override
@SuppressWarnings("unchecked")
public <T> T service(Class<T> serviceType)
{
T svc = (T) services.get(serviceType);
if (svc == null)
throw new IllegalArgumentException("Service not available in this context: " + serviceType.getName());
return svc;
}
@Override
public Set<Class<?>> services()
{
return services.keySet();
}
}
public static class CommandResult
{
private final UUID executionId;
private final String output;
private final long startTime;
private final long durationMillis;
public CommandResult(UUID executionId, String output, long startTime, long durationMillis)
{
this.executionId = executionId;
this.output = output;
this.startTime = startTime;
this.durationMillis = durationMillis;
}
public UUID getExecutionId()
{
return executionId;
}
public String getOutput()
{
return output;
}
public long getStartTime()
{
return startTime;
}
public long getDurationMillis()
{
return durationMillis;
}
}
/**
* Record of a single command execution. Retained in {@link BoundedExecutionHistory} and is
* surfaced to operators as command execution history via virtual tables.
*/
static class ExecutionHistory
{
final UUID executionId;
final String commandName;
final long startTime;
volatile long endTime;
volatile boolean success;
volatile Throwable error;
ExecutionHistory(UUID executionId, String commandName, long startTime)
{
this.executionId = executionId;
this.commandName = commandName;
this.startTime = startTime;
}
void completed(long endTime)
{
this.endTime = endTime;
this.success = true;
}
void failed(long endTime, Throwable error)
{
this.endTime = endTime;
this.success = false;
this.error = error;
}
public UUID executionId() { return executionId; }
public String commandName() { return commandName; }
public boolean isSuccess() { return success; }
public Throwable error() { return error; }
}
private static class BoundedExecutionHistory
{
private final Deque<ExecutionHistory> dq = new ConcurrentLinkedDeque<>();
private final AtomicInteger size = new AtomicInteger(0);
private final int maxSize;
public BoundedExecutionHistory(int maxSize)
{
this.maxSize = maxSize;
}
public void add(ExecutionHistory info)
{
dq.offer(info);
if (size.incrementAndGet() > maxSize)
{
ExecutionHistory removed = dq.pollFirst();
if (removed != null)
size.decrementAndGet();
}
}
/** Returns a point-in-time snapshot of the retained records, oldest first. */
public List<ExecutionHistory> snapshot()
{
return new ArrayList<>(dq);
}
}
@FunctionalInterface
public interface Executor
{
CommandResult execute(String commandName, Supplier<CommandExecutionArgs> argsSupplier)
throws CommandExecutionException, CommandValidationException, CommandAuthorizationException;
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.management;
/**
* MBean interface for CommandInvokerService.
* Exposes registry-level operations (not individual commands).
*/
public interface CommandInvokerServiceMBean
{
String MBEAN_NAME = "org.apache.cassandra.management:type=CommandInvokerService";
/**
* Get a list of all command names in the registry.
* @return array of command names
*/
String[] getCommandNames();
/**
* Get the total number of commands in the registry.
* @return number of commands
*/
int getCommandCount();
/**
* Get ObjectName string for a specific command MBean.
* @param fullCommandName name of the command
* @return ObjectName string for the command MBean
* @throws IllegalArgumentException if command not found
*/
String getCommandMBeanName(String fullCommandName);
}

View File

@ -0,0 +1,349 @@
/*
* 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.management;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.ReflectionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.management.api.ArgumentMetadata;
import org.apache.cassandra.management.api.Command;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.management.api.OptionMetadata;
import org.apache.cassandra.management.api.ParameterMetadata;
import org.apache.cassandra.utils.JsonUtils;
import static javax.management.MBeanOperationInfo.ACTION;
import static javax.management.MBeanOperationInfo.INFO;
import static org.apache.cassandra.cql3.statements.ExecuteCommandStatement.COMMAND_RESULT_SCHEMA_EXECUTION_ID;
import static org.apache.cassandra.cql3.statements.ExecuteCommandStatement.COMMAND_RESULT_SCHEMA_OUTPUT;
import static org.apache.cassandra.management.api.ParameterMetadata.COMMAND_POSITIONAL_PARAM_PREFIX;
import static org.apache.cassandra.utils.JsonUtils.convertDefaultValue;
import static org.apache.cassandra.utils.JsonUtils.getJsonType;
/**
* Command MBean exposes a single management command to the JMX interface.
*
* <p>Uses JSON-based parameter format:
* <ul>
* <li>Single "invoke" operation with JSON string parameter</li>
* <li>JSON format: {"optionName": "value", "param0": "value", ...}</li>
* <li>Option names: use option name or any alias (e.g., "concurrent-compactors", "--concurrent-compactors")</li>
* <li>Positional parameters: use "param0", "param1", etc. or parameter name</li>
* <li>Returns command output as String</li>
* </ul>
*
* <p>
* Invocation:
* <pre>
* // Command: setconcurrentcompactors --concurrent-compactors 4
* mbean.invoke("invoke",
* new Object[]{"{\"concurrent-compactors\": \"4\"}"},
* new String[]{ "String" });
*
* // Command: getendpoints keyspace table
* mbean.invoke("invoke",
* new Object[]{"{\"param0\": \"mykeyspace\", \"param1\": \"mytable\"}"},
* new String[]{ "String" });
* </pre>
*
* <p>The {@code invoke} operation accepts exactly one argument: the JSON string above. Other forms
* (e.g. name-value pairs spread across multiple arguments) are not supported and are rejected with an
* {@link IllegalArgumentException}.
*/
public class CommandMBeanAdapter implements DynamicMBean
{
public static final String INVOKE_METHOD = "invoke";
public static final String GET_JSON_SCHEMA_METHOD = "getJsonSchema";
public static final String GET_PARAMETER_INFO_METHOD = "getParameterInfo";
private static final Logger logger = LoggerFactory.getLogger(CommandMBeanAdapter.class);
private final String fullCommandName;
private final CommandMetadata metadata;
private final CommandInvokerService.Executor executor;
public CommandMBeanAdapter(String fullCommandName, Command<?> command, CommandInvokerService.Executor executor)
{
this.fullCommandName = fullCommandName;
this.metadata = command.metadata();
this.executor = executor;
}
@Override
public MBeanInfo getMBeanInfo()
{
List<MBeanOperationInfo> operations = new ArrayList<>();
operations.add(new MBeanOperationInfo(
INVOKE_METHOD,
"Execute command with JSON parameters. Format: {\"optionName\": \"value\", \"param0\": \"value\", ...}",
new MBeanParameterInfo[]{
new MBeanParameterInfo("jsonParameters",
String.class.getName(),
"JSON object with command parameters. Use getJsonSchema() to see available parameters and types.")
},
String.class.getName(),
ACTION));
operations.add(new MBeanOperationInfo(
GET_JSON_SCHEMA_METHOD,
"Get JSON schema describing all available parameters (name -> type mapping)",
new MBeanParameterInfo[0],
String.class.getName(),
INFO));
operations.add(new MBeanOperationInfo(
GET_PARAMETER_INFO_METHOD,
"Get information about all available parameters (human-readable format)",
new MBeanParameterInfo[0],
String.class.getName(),
INFO));
return new MBeanInfo(CommandMBeanAdapter.class.getName(),
metadata.description() != null ? metadata.description() : "Command: " + metadata.name(),
null,
null,
operations.toArray(new MBeanOperationInfo[0]),
null);
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, ReflectionException
{
if (INVOKE_METHOD.equals(actionName))
{
if (params == null || params.length != 1 || !(params[0] instanceof String))
throw new IllegalArgumentException("invoke requires exactly one parameter (JSON string)");
String jsonParams = (String) params[0];
try
{
CommandInvokerService.CommandResult result = executor.execute(fullCommandName,
() -> CommandExecutionArgsSerde.fromJson(jsonParams, metadata));
Map<String, Object> jsonResult = new TreeMap<>();
jsonResult.put(COMMAND_RESULT_SCHEMA_EXECUTION_ID, result.getExecutionId().toString());
jsonResult.put(COMMAND_RESULT_SCHEMA_OUTPUT, result.getOutput());
return JsonUtils.writeAsJsonString(jsonResult);
}
catch (CommandAuthorizationException e)
{
logger.error("Authorization error executing command: {}", metadata.name(), e);
throw new SecurityException("Access Denied: " + e.getMessage());
}
catch (CommandValidationException e)
{
logger.error("Validation error for command: {}", metadata.name(), e);
throw new IllegalArgumentException(ManagementUtils.causeMessages(e));
}
catch (CommandExecutionException e)
{
logger.error("Error executing command: {}", metadata.name(), e);
throw new RuntimeException(ManagementUtils.causeMessages(e));
}
catch (Exception e)
{
logger.error("Unexpected error executing command: {}", metadata.name(), e);
throw new RuntimeException("Unexpected error executing command: " + e.getMessage(), e);
}
}
if (GET_JSON_SCHEMA_METHOD.equals(actionName))
return getJsonSchema();
if (GET_PARAMETER_INFO_METHOD.equals(actionName))
return getParameterInfo();
throw new UnsupportedOperationException("Unknown operation: " + actionName);
}
public String getJsonSchema()
{
try
{
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("$schema", "http://json-schema.org/draft-07/schema#");
schema.put("type", "object");
schema.put("title", metadata.name());
schema.put("description", metadata.description());
Map<String, Object> properties = new LinkedHashMap<>();
List<String> required = new ArrayList<>();
for (OptionMetadata option : metadata.options())
{
String primaryName = option.paramLabel();
properties.put(primaryName, buildJsonSchemaProperty(option));
if (option.required())
required.add(primaryName);
}
List<ParameterMetadata> sortedParams = new ArrayList<>(metadata.parameters());
sortedParams.sort(Comparator.comparingInt(ParameterMetadata::index));
for (ParameterMetadata param : sortedParams)
{
String paramName = COMMAND_POSITIONAL_PARAM_PREFIX + param.index();
properties.put(paramName, buildJsonSchemaProperty(param));
if (param.required())
required.add(paramName);
}
schema.put("properties", properties);
if (!required.isEmpty())
schema.put("required", required);
return JsonUtils.writeAsPrettyJsonString(schema);
}
catch (Exception e)
{
logger.error("Error generating JSON schema for command: {}", metadata.name(), e);
throw new RuntimeException("Failed to generate JSON schema: " + e.getMessage(), e);
}
}
private Map<String, Object> buildJsonSchemaProperty(ArgumentMetadata arg)
{
Map<String, Object> prop = new LinkedHashMap<>();
prop.put("type", getJsonType(arg.type()));
if (arg.names() != null && arg.names().length > 0)
prop.put("aliases", Arrays.stream(arg.names())
.filter(name -> !name.equals(arg.paramLabel()))
.collect(Collectors.toList()));
if (arg.description() != null && !arg.description().isEmpty())
prop.put("description", arg.description());
String defaultValue = arg.defaultValue();
if (defaultValue != null && !defaultValue.isEmpty())
prop.put("default", convertDefaultValue(defaultValue, arg.type()));
if (arg.type().isArray() || List.class.isAssignableFrom(arg.type()))
{
prop.put("type", "array");
Map<String, Object> items = new LinkedHashMap<>();
items.put("type", "string");
prop.put("items", items);
}
if (arg.type().isEnum())
{
prop.put("enum", Arrays.stream(arg.type().getEnumConstants())
.map(Object::toString)
.collect(Collectors.toList()));
}
return prop;
}
private static String getTypeName(Class<?> type)
{
return type.getCanonicalName();
}
private String getParameterInfo()
{
StringBuilder info = new StringBuilder();
info.append("Command: ").append(metadata.name()).append('\n');
info.append("Description: ").append(metadata.description()).append("\n\n");
info.append("Options:\n");
for (OptionMetadata option : metadata.options())
{
info.append(" - ").append(option.paramLabel());
if (option.names().length > 0)
info.append(" (aliases: ").append(String.join(", ", option.names())).append(')');
appendRequiredClause(info, getTypeName(option.type()), option.required(), option.description());
}
info.append("\nPositional Parameters:\n");
List<ParameterMetadata> sortedParams = new ArrayList<>(metadata.parameters());
sortedParams.sort((a, b) -> Integer.compare(a.index(), b.index()));
for (ParameterMetadata param : sortedParams)
{
info.append(" - param").append(param.index());
if (param.paramLabel() != null && !param.paramLabel().isEmpty())
info.append(" (").append(param.paramLabel()).append(')');
appendRequiredClause(info, getTypeName(param.type()), param.required(), param.description());
}
return info.toString();
}
private static void appendRequiredClause(StringBuilder info,
String typeName,
boolean required,
String description)
{
info.append(" [").append(typeName).append(']');
if (required)
info.append(" [REQUIRED]");
if (description != null && !description.isEmpty())
info.append("\n ").append(description);
info.append('\n');
}
@Override
public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException
{
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException
{
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public AttributeList getAttributes(String[] attributes)
{
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public AttributeList setAttributes(AttributeList attributes)
{
throw new UnsupportedOperationException("Not supported yet.");
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.management;
public class CommandValidationException extends Exception
{
public CommandValidationException(String message, Throwable cause)
{
super(message, cause);
}
}

View File

@ -0,0 +1,508 @@
/*
* 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.management;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.RuntimeMXBean;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.audit.AuditLogManagerMBean;
import org.apache.cassandra.auth.AbstractCIDRAuthorizer;
import org.apache.cassandra.auth.AuthCache;
import org.apache.cassandra.auth.AuthCacheMBean;
import org.apache.cassandra.auth.AuthCacheService;
import org.apache.cassandra.auth.CIDRGroupsMappingManagerMBean;
import org.apache.cassandra.auth.CIDRPermissionsManagerMBean;
import org.apache.cassandra.auth.NetworkPermissionsCacheMBean;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.auth.PermissionsCacheMBean;
import org.apache.cassandra.auth.RolesCacheMBean;
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
import org.apache.cassandra.batchlog.BatchlogManager;
import org.apache.cassandra.batchlog.BatchlogManagerMBean;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
import org.apache.cassandra.db.compression.CompressionDictionaryManager;
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.guardrails.GuardrailsMBean;
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable;
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTableMBean;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.gms.FailureDetectorMBean;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.GossiperMBean;
import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.hints.HintsServiceMBean;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.SecondaryIndexManager;
import org.apache.cassandra.locator.DynamicEndpointSnitch;
import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
import org.apache.cassandra.locator.EndpointSnitchInfo;
import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
import org.apache.cassandra.locator.LocationInfo;
import org.apache.cassandra.locator.LocationInfoMBean;
import org.apache.cassandra.locator.NodeProximity;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.metrics.ThreadPoolMetrics;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.MessagingServiceMBean;
import org.apache.cassandra.profiler.AsyncProfilerMBean;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.service.ActiveRepairServiceMBean;
import org.apache.cassandra.service.AsyncProfilerService;
import org.apache.cassandra.service.AutoRepairService;
import org.apache.cassandra.service.AutoRepairServiceMBean;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.service.CacheServiceMBean;
import org.apache.cassandra.service.GCInspector;
import org.apache.cassandra.service.GCInspectorMXBean;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageProxyMBean;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.StorageServiceMBean;
import org.apache.cassandra.service.accord.AccordOperations;
import org.apache.cassandra.service.accord.AccordOperationsMBean;
import org.apache.cassandra.service.snapshot.SnapshotManager;
import org.apache.cassandra.service.snapshot.SnapshotManagerMBean;
import org.apache.cassandra.streaming.StreamManager;
import org.apache.cassandra.streaming.StreamManagerMBean;
import org.apache.cassandra.tcm.CMSOperations;
import org.apache.cassandra.tcm.CMSOperationsMBean;
import org.apache.cassandra.tools.RemoteJmxMBeanAccessor;
import org.apache.cassandra.utils.MBeanWrapper;
import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
import static org.apache.cassandra.service.CassandraDaemon.SKIP_GC_INSPECTOR;
/**
* Server-side implementation of {@link MBeanAccessor} for in-process execution.
*
* <p>
* This implementation provides direct access to MBean instances without using JMX,
* eliminating the need for remote connections or JMX proxies. It is designed for
* server-side command execution as part of CEP-38 Management API, where commands run
* in the same JVM as the Cassandra daemon.
*
* <p>
* Unlike {@link RemoteJmxMBeanAccessor}, this implementation:
* <ul>
* <li>Directly accesses singleton instances (e.g., {@code StorageService.instance})</li>
* <li>Does not require network connections or JMX connectors</li>
* <li>Provides better performance by avoiding serialization/deserialization</li>
* <li>Has no connection state to manage</li>
* <li>Works directly with {@link Keyspace} and {@link ColumnFamilyStore} instances</li>
* </ul>
*
* <p>
* <b>Lazy Initialization</b>: MBean providers are initialized registered for known MBeans.
*
* @see MBeanAccessor
* @see RemoteJmxMBeanAccessor
* @since 5.1
*/
public class InternalNodeMBeanAccessor implements MBeanAccessor
{
private final Map<Class<?>, MBeanProvider<?>> mBeanProviders = new ConcurrentHashMap<>();
private final Map<Class<?>, Object> mBeanCache = new ConcurrentHashMap<>();
private final Map<String, Object> metricCache = new ConcurrentHashMap<>();
/**
* Creates a new InternalNodeMBeanAccessor using direct instance access.
*/
public InternalNodeMBeanAccessor()
{
initializeMBeanProviders();
}
/**
* Initializes all statically known MBean instances.
*/
private void initializeMBeanProviders()
{
registerMBeanProvider(AccordOperationsMBean.class, () -> AccordOperations.instance);
registerMBeanProvider(ActiveRepairServiceMBean.class, ActiveRepairService::instance);
registerMBeanProvider(AuditLogManagerMBean.class, () -> AuditLogManager.instance);
registerMBeanProvider(AutoRepairServiceMBean.class, () -> AutoRepairService.instance);
registerMBeanProvider(BatchlogManagerMBean.class, () -> BatchlogManager.instance);
registerMBeanProvider(CMSOperationsMBean.class, () -> CMSOperations.instance);
registerMBeanProvider(CacheServiceMBean.class, () -> CacheService.instance);
registerMBeanProvider(CompactionManagerMBean.class, () -> CompactionManager.instance);
registerMBeanProvider(DynamicEndpointSnitchMBean.class, this::resolveDynamicEndpointSnitch);
registerMBeanProvider(FailureDetectorMBean.class, () -> (FailureDetectorMBean) FailureDetector.instance);
registerMBeanProvider(GCInspectorMXBean.class, this::resolveGCInspector);
registerMBeanProvider(GossiperMBean.class, () -> Gossiper.instance);
registerMBeanProvider(GuardrailsMBean.class, () -> Guardrails.instance);
registerMBeanProvider(HintsServiceMBean.class, () -> HintsService.instance);
registerMBeanProvider(MemoryMXBean.class, ManagementFactory::getMemoryMXBean);
registerMBeanProvider(MessagingServiceMBean.class, MessagingService::instance);
registerMBeanProvider(RuntimeMXBean.class, ManagementFactory::getRuntimeMXBean);
registerMBeanProvider(SnapshotManagerMBean.class, () -> SnapshotManager.instance);
registerMBeanProvider(StorageProxyMBean.class, () -> StorageProxy.instance);
registerMBeanProvider(StorageServiceMBean.class, () -> StorageService.instance);
registerMBeanProvider(StreamManagerMBean.class, () -> StreamManager.instance);
registerMBeanProvider(AsyncProfilerMBean.class, AsyncProfilerService::instance);
// Utility MBeans are stateless and can be created on demand.
// They query DatabaseDescriptor for the current state, so new instances are fine
registerMBeanProvider(EndpointSnitchInfoMBean.class, EndpointSnitchInfo::new);
registerMBeanProvider(LocationInfoMBean.class, LocationInfo::new);
// AuthCache MBeans
registerMBeanProvider(AuthorizationProxy.JmxPermissionsCacheMBean.class,
() -> findAuthCache(AuthorizationProxy.JmxPermissionsCacheMBean.class));
registerMBeanProvider(NetworkPermissionsCacheMBean.class,
() -> findAuthCache(NetworkPermissionsCacheMBean.class));
registerMBeanProvider(PasswordAuthenticator.CredentialsCacheMBean.class,
() -> findAuthCache(PasswordAuthenticator.CredentialsCacheMBean.class));
registerMBeanProvider(PermissionsCacheMBean.class,
() -> findAuthCache(PermissionsCacheMBean.class));
registerMBeanProvider(RolesCacheMBean.class,
() -> findAuthCache(RolesCacheMBean.class));
// CIDR Auth MBeans
registerMBeanProvider(CIDRFilteringMetricsTableMBean.class, () -> CIDRFilteringMetricsTable.instance);
registerMBeanProvider(CIDRGroupsMappingManagerMBean.class, () -> AbstractCIDRAuthorizer.cidrGroupsMappingManager);
registerMBeanProvider(CIDRPermissionsManagerMBean.class, () -> AbstractCIDRAuthorizer.cidrPermissionsManager);
}
/**
* Gets DynamicEndpointSnitch from DatabaseDescriptor if it's a DynamicEndpointSnitch,
* otherwise returns null.
*/
private DynamicEndpointSnitchMBean resolveDynamicEndpointSnitch()
{
if (!DatabaseDescriptor.isDynamicEndpointSnitch())
throw new IllegalStateException("DynamicEndpointSnitch has been requested but is not enabled");
NodeProximity proximity = DatabaseDescriptor.getNodeProximity();
assert proximity instanceof DynamicEndpointSnitch;
return (DynamicEndpointSnitchMBean) proximity;
}
private GCInspectorMXBean resolveGCInspector()
{
if (SKIP_GC_INSPECTOR)
throw new IllegalStateException("GCInspector has been requested but is disabled via SKIP_GC_INSPECTOR flag");
try
{
MBeanServer mbs = MBeanWrapper.instance.getMBeanServer();
if (mbs == null)
return null;
ObjectName name = new ObjectName(GCInspector.MBEAN_NAME);
if (mbs.isRegistered(name))
return JMX.newMBeanProxy(mbs, name, GCInspectorMXBean.class);
}
catch (Exception e)
{
// Fall through to create a new instance
}
return null;
}
/** Finds an auth cache MBean instance from AuthCacheService. */
private <T> T findAuthCache(Class<T> clazz)
{
Set<AuthCache<?, ?>> caches = AuthCacheService.instance.getCaches();
if (caches.isEmpty())
return null;
AuthCacheFinder visitor = new AuthCacheFinder(clazz);
for (AuthCache<?, ?> cache : caches)
{
cache.accept(visitor);
Object found = visitor.getCache();
if (found == null)
continue;
return clazz.cast(found);
}
return null;
}
private <T> void registerMBeanProvider(Class<T> clazz, MBeanProvider<T> locator)
{
Object prev = mBeanProviders.putIfAbsent(clazz, locator);
assert prev == null : "MBean locator for " + clazz.getName() + " is already registered";
}
@Override
public <T> T findMBean(Class<T> clazz)
{
Object cached = mBeanCache.get(clazz);
if (cached != null)
return clazz.cast(cached);
Object created = mBeanCache.computeIfAbsent(clazz, k -> {
MBeanProvider<?> provider = mBeanProviders.get(k);
return provider == null ? null : provider.provide();
});
return created == null ? null : clazz.cast(created);
}
private MBeanServer mbeanServer()
{
MBeanServer mbs = MBeanWrapper.instance.getMBeanServer();
if (mbs == null)
throw new IllegalStateException("The MBean server is not available (MBean registration is disabled on this node)");
return mbs;
}
@Override
public <T> T findMBeanMetric(Class<T> clazz, Props props)
{
try
{
assert clazz.isInterface() && CassandraMetricsRegistry.MetricMBean.class.isAssignableFrom(clazz);
ObjectName objectName = buildObjectNameFromProps(props);
String cacheKey = objectName.getCanonicalName();
@SuppressWarnings("unchecked")
T cached = (T) metricCache.get(cacheKey);
if (cached != null)
return cached;
MBeanServer mbs = mbeanServer();
return clazz.cast(metricCache.computeIfAbsent(cacheKey, k -> JMX.newMBeanProxy(mbs, objectName, clazz)));
}
catch (Exception e)
{
throw new RuntimeException("Error accessing metric MBean: " + e.getMessage(), e);
}
}
@Override
public boolean isMBeanMetricRegistered(Props props)
{
try
{
// Use the internal MBean server to look up MBean by ObjectName. This leverages existing
// JMX infrastructure and avoids the complexity of constructing metric names from Props.
//
// Alternatively, we could use CassandraMetricsRegistry to look up metrics by metric name
// (e.g., "org.apache.cassandra.metrics.Keyspace.ReadLatency.mykeyspace"), but this requires
// constructing the full metric name from Props, which is problematic.
//
// The reconstructing the scope problem: Different MetricNameFactory implementations
// construct scopes differently:
// - KeyspaceMetrics: scope = keyspace property
// - TableMetrics: scope = keyspace + '.' + scope property
// - DefaultNameFactory: scope = scope property
// - SAI AbstractMetrics: scope = keyspace.table.index.scope (all combined)
//
// To construct metric names from Props, we would need to duplicate scope construction logic
// from each factory or refactor to share it. Using ObjectName, in turn, avoids this. We query
// MBeanServer using the ObjectName pattern already constructed by factories during registration.
//
// However, it will be beneficial to revisit this down the line for performance optimizations and
// to avoid JMX entanglement, so we could switch it off for in-process access.
ObjectName objectName = buildObjectNameFromProps(props);
return mbeanServer().isRegistered(objectName);
}
catch (Exception e)
{
throw new RuntimeException("Error checking metric MBean registration: " + e.getMessage(), e);
}
}
private static ObjectName buildObjectNameFromProps(Props props) throws MalformedObjectNameException
{
return new ObjectName("org.apache.cassandra.metrics", new Hashtable<>(props.toMap()));
}
@Override
public ColumnFamilyStoreMBean findColumnFamily(String type, String keyspace, String columnFamily)
{
Keyspace ks = Schema.instance.getKeyspaceInstance(keyspace);
if (ks == null)
throw new IllegalArgumentException("Keyspace not found: " + keyspace);
if (!SecondaryIndexManager.isIndexColumnFamily(columnFamily))
return ks.getColumnFamilyStore(columnFamily);
// Secondary-index stores are registered under "base.index" names and are only
// reachable through the base table's index manager, not by keyspace lookup.
ColumnFamilyStore base = ks.getColumnFamilyStore(SecondaryIndexManager.getParentCfsName(columnFamily));
Index index = base.indexManager.getIndexByName(SecondaryIndexManager.getIndexName(columnFamily));
if (index == null)
throw new IllegalArgumentException(String.format("Index not found: %s.%s", keyspace, columnFamily));
return index.getBackingTable()
.orElseThrow(() -> new IllegalArgumentException(String.format("Index %s.%s has no backing table",
keyspace, columnFamily)));
}
@Override
public CompressionDictionaryManagerMBean findCompressionDictionary(String keyspace, String table)
{
Keyspace ks = Schema.instance.getKeyspaceInstance(keyspace);
if (ks == null || Schema.instance.getTableMetadata(keyspace, table) == null)
throw new IllegalArgumentException(String.format("Table %s.%s does not exist", keyspace, table));
CompressionDictionaryManager manager = ks.getColumnFamilyStore(table).compressionDictionaryManager();
if (!manager.isEnabled())
throw new IllegalStateException("The compression on table " + keyspace + '.' + table +
" is not enabled or SSTable compressor is not a dictionary compressor.");
return manager;
}
@Override
public List<ThreadPoolInfo> threadPoolInfos()
{
List<ThreadPoolInfo> infos = new ArrayList<>();
for (ThreadPoolMetrics metrics : Metrics.allThreadPoolMetrics())
infos.add(new ThreadPoolInfo(metrics.path, metrics.poolName));
return infos;
}
@Override
public List<Map.Entry<String, ColumnFamilyStoreMBean>> findColumnFamilies(String type)
{
try
{
assert type.equals("IndexColumnFamilies") || type.equals("ColumnFamilies");
List<Map.Entry<String, ColumnFamilyStoreMBean>> mbeans = new ArrayList<>();
for (Keyspace keyspace : Keyspace.all())
{
for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores())
{
// Secondary-index backing stores are not registered in the keyspace,
// they are only reachable through the base table's index manager.
if (type.equals("IndexColumnFamilies"))
for (ColumnFamilyStore indexCfs : cfs.indexManager.getAllIndexColumnFamilyStores())
mbeans.add(new AbstractMap.SimpleImmutableEntry<>(keyspace.getName(), indexCfs));
else
mbeans.add(new AbstractMap.SimpleImmutableEntry<>(keyspace.getName(), cfs));
}
}
return mbeans;
}
catch (Exception e)
{
throw new RuntimeException("Error accessing column families", e);
}
}
@Override
public void close()
{
metricCache.clear();
mBeanCache.clear();
}
/**
* Functional interface for providing MBean instances lazily.
* Used to defer MBean initialization until the MBean is actually accessed.
*
* @param <T> the MBean interface type
*/
@FunctionalInterface
public interface MBeanProvider<T>
{
/**
* @return the MBean instance, or {@code null} if the MBean is not available
* @throws RuntimeException if the MBean cannot be provided (e.g., not initialized yet)
*/
T provide();
}
/** Visitor that finds a specific auth cache MBean type from AuthCacheService. */
private static class AuthCacheFinder implements AuthCache.MBeanVisitor
{
private final Class<?> targetType;
private Object foundCache;
AuthCacheFinder(Class<?> targetType)
{
this.targetType = targetType;
}
@Override
public void visitCredentials(PasswordAuthenticator.CredentialsCacheMBean cache)
{
if (targetType.equals(PasswordAuthenticator.CredentialsCacheMBean.class))
foundCache = cache;
}
@Override
public void visitJmxPermissions(AuthorizationProxy.JmxPermissionsCacheMBean cache)
{
if (targetType.equals(AuthorizationProxy.JmxPermissionsCacheMBean.class))
foundCache = cache;
}
@Override
public void visitPermissions(PermissionsCacheMBean cache)
{
if (targetType.equals(PermissionsCacheMBean.class))
foundCache = cache;
}
@Override
public void visitNetwork(NetworkPermissionsCacheMBean cache)
{
if (targetType.equals(NetworkPermissionsCacheMBean.class))
foundCache = cache;
}
@Override
public void visitRoles(RolesCacheMBean cache)
{
if (targetType.equals(RolesCacheMBean.class))
foundCache = cache;
}
@Override
public void visit(AuthCacheMBean cache)
{
// No-op. Used for caches without specific MBean types.
}
Object getCache()
{
return foundCache;
}
}
}

View File

@ -0,0 +1,165 @@
/*
* 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.management;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
public interface MBeanAccessor extends AutoCloseable
{
/**
* Finds statically known MBeans by MBean class name.
* @param clazz the MBean class.
* @return MBean class or {@code null} if not found.
* @param <T> the MBean type.
*/
@Nullable
<T> T findMBean(Class<T> clazz);
<T> T findMBeanMetric(Class<T> clazz, Props props);
boolean isMBeanMetricRegistered(Props props);
ColumnFamilyStoreMBean findColumnFamily(String type, String keyspace, String columnFamily);
CompressionDictionaryManagerMBean findCompressionDictionary(String keyspace, String table);
/** List of thread pool MBeans with associated path and pool name. */
List<ThreadPoolInfo> threadPoolInfos();
/** List of column family MBeans with associated keyspace name. */
List<Map.Entry<String, ColumnFamilyStoreMBean>> findColumnFamilies(String type);
/** {@inheritDoc} */
@Override
default void close() { }
default CassandraMetricsRegistry.JmxCounterMBean findMBeanCounter(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxCounterMBean.class, props);
}
default CassandraMetricsRegistry.JmxGaugeMBean findMBeanGauge(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxGaugeMBean.class, props);
}
default CassandraMetricsRegistry.JmxMeterMBean findMBeanMeter(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxMeterMBean.class, props);
}
default CassandraMetricsRegistry.JmxTimerMBean findMBeanTimer(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxTimerMBean.class, props);
}
default CassandraMetricsRegistry.JmxHistogramMBean findMBeanHistogram(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxHistogramMBean.class, props);
}
class Props
{
private final Map<String, String> values = Maps.newLinkedHashMap();
public Props(String type, String path, String keyspace, String table, String scope, String name)
{
if (type == null || type.isEmpty())
throw new IllegalArgumentException("type is required");
values.put("type", type);
if (path != null)
values.put("path", path);
if (keyspace != null)
values.put("keyspace", keyspace);
if (table != null)
values.put("table", table);
if (scope != null)
values.put("scope", scope);
if (name != null)
values.put("name", name);
}
public static Props metric(String type, String name)
{
return new Props(type, null, null, null, null, name);
}
public static Props scoped(String type, String scope, String name)
{
return new Props(type, null, null, null, scope, name);
}
public static Props threadPool(String type, String path, String scope, String name)
{
return new Props(type, path, null, null, scope, name);
}
public static Props sai(String type, String keyspace, String table, String scope, String name)
{
return new Props(type, null, keyspace, table, scope, name);
}
public static Props columnFamily(String type, String keyspace, String scope, String name)
{
return new Props(type, null, keyspace, null, scope, name);
}
public static Props keyspace(String type, String keyspace, String name)
{
return new Props(type, null, keyspace, null, null, name);
}
public Map<String, String> toMap()
{
return ImmutableMap.copyOf(values);
}
}
class ThreadPoolInfo
{
private final String path;
private final String poolName;
public ThreadPoolInfo(String path, String poolName)
{
this.path = path;
this.poolName = poolName;
}
public String path()
{
return path;
}
public String poolName()
{
return poolName;
}
public String scope()
{
return path + '.' + poolName;
}
}
}

View File

@ -0,0 +1,157 @@
/*
* 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.management;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Function;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import org.apache.cassandra.management.api.Command;
import org.apache.cassandra.management.api.CommandRegistry;
import picocli.CommandLine;
public final class ManagementUtils
{
public static final String COMMAND_NAME_DELIMITER = ".";
public static <S> Iterable<S> loadService(Class<S> serviceClz)
{
return AccessController.doPrivileged((PrivilegedAction<Iterable<S>>) () -> ServiceLoader.load(serviceClz));
}
public static int countCommands(CommandRegistry registry)
{
int count = 0;
for (Map.Entry<String, Command<?>> entry : registry.commands())
{
Command<?> cmd = entry.getValue();
if (cmd instanceof CommandRegistry)
count += countCommands((CommandRegistry) cmd);
else
count++;
}
return count;
}
public static String stripAngleBrackets(String name)
{
if (name == null || name.isEmpty())
return name;
String trimmed = name.trim();
if (trimmed.length() >= 2 &&
trimmed.charAt(0) == '<' &&
trimmed.charAt(trimmed.length() - 1) == '>')
{
return trimmed.substring(1, trimmed.length() - 1).trim();
}
return name;
}
/** Normalize the option name by stripping leading dashes. */
public static String normalizeOptionName(String name)
{
if (name.startsWith("--"))
return name.substring(2);
else if (name.startsWith("-"))
return name.substring(1);
return name;
}
/**
* Builds a concise, client-safe error string from a throwable's cause chain: the distinct, non-empty
* messages of the throwable and its causes, joined by {@code ": "}. Unlike a full stack trace, this
* exposes only the human-readable messages (which command validation/usage errors rely on) without
* leaking internal class names, file paths or line numbers. The full stack trace is logged separately
* server-side.
*/
public static String causeMessages(Throwable t)
{
Set<String> messages = new LinkedHashSet<>();
for (Throwable cause : Throwables.getCausalChain(t))
{
String message = cause.getMessage();
if (Strings.isNullOrEmpty(message))
continue;
// Skip a message already represented by an ancestor.
if (messages.stream().anyMatch(existing -> existing.contains(message)))
continue;
messages.add(message);
}
return messages.isEmpty() ? t.getClass().getSimpleName() : String.join(": ", messages);
}
public static String fullCommandName(String parent, String name)
{
return Strings.isNullOrEmpty(parent) ? name : String.join(COMMAND_NAME_DELIMITER, parent, name);
}
public static <T> String fullCommandName(List<T> parentCommands, Function<T, String> nameExtractor)
{
StringBuilder sb = new StringBuilder();
for (T part : parentCommands)
{
String extracted = nameExtractor.apply(part);
if (Strings.isNullOrEmpty(extracted))
continue;
if (sb.length() > 0)
sb.append(COMMAND_NAME_DELIMITER);
sb.append(extracted);
}
return sb.length() > 0 ? sb.toString() : null;
}
@SuppressWarnings("unchecked")
public static <T> T findRegistryCommand(String fullCommandName, CommandRegistry registry)
{
String[] parts = fullCommandName.split('\\' + COMMAND_NAME_DELIMITER, 2);
Command<?> cmd = registry.command(parts[0]);
if (cmd == null)
return null;
if (parts.length == 1)
return (T) cmd;
if (cmd instanceof CommandRegistry)
return findRegistryCommand(parts[1], (CommandRegistry) cmd);
return null;
}
public static Class<?> elementType(CommandLine.Model.ArgSpec spec)
{
Class<?> type = spec.type();
if (!type.isArray() && !java.util.Collection.class.isAssignableFrom(type))
return null;
Class<?>[] auxiliaryTypes = spec.auxiliaryTypes();
return auxiliaryTypes == null || auxiliaryTypes.length == 0 ? null : auxiliaryTypes[0];
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.management;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.api.OptionMetadata;
import org.apache.cassandra.management.api.ParameterMetadata;
public class SimpleCommandExecutionArgs implements CommandExecutionArgs
{
private final Map<OptionMetadata, Object> options;
private final Map<ParameterMetadata, Object> parameters;
public SimpleCommandExecutionArgs(Map<OptionMetadata, Object> options,
Map<ParameterMetadata, Object> parameters)
{
this.options = new LinkedHashMap<>(options);
this.parameters = new LinkedHashMap<>(parameters);
}
@Override
public Map<ParameterMetadata, Object> parameters()
{
return parameters;
}
@Override
public Map<OptionMetadata, Object> options()
{
return options;
}
@Override
public boolean hasParameter(ParameterMetadata param)
{
return parameters.containsKey(param);
}
@Override
public boolean hasOption(OptionMetadata option)
{
return options.containsKey(option);
}
@Override
public String toString()
{
return "SimpleCommandExecutionArgs{" +
"options=" + options +
", parameters=" + parameters +
'}';
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.management.api;
public interface ArgumentMetadata
{
String[] names();
String paramLabel();
String description();
Class<?> type();
String defaultValue();
boolean required();
/** "0" (flag), "1", "0..1", "0..*" */
String arity();
}

View File

@ -0,0 +1,53 @@
/*
* 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.management.api;
/**
* A management command following the Command java-pattern. Each command is a self-describing,
* executable unit that exposes its {@link CommandMetadata} and can be invoked uniformly by
* the {@link org.apache.cassandra.management.CommandInvokerService} regardless of the
* transport: JMX or CQL.
* <p>
* The type parameter {@code R} is the structured result type returned by
* {@link #execute(CommandExecutionArgs, CommandExecutionContext)}. Existing picocli-based
* commands return {@code Void} and write output via services obtained from the context;
* new commands may return a typed result for protocol-level serialization.
*
* @param <R> the command result type, or {@link Void} for commands that only produce
* side-effect output through the execution context
* @see CommandRegistry
*/
public interface Command<R>
{
/** Get command metadata - replaces argClass() from the cep-38 doc with richer information. */
CommandMetadata metadata();
/**
* Execute the command.
*
* @param arguments command arguments.
* @param context execution context providing access to services via {@link CommandExecutionContext#service(Class)}.
* @return structured data result, or {@code null} for commands that only produce
* side effect output (e.g. existing picocli-based commands).
*/
R execute(CommandExecutionArgs arguments, CommandExecutionContext context);
default String name() { return metadata().name(); }
default String description() { return metadata().description(); }
}

View File

@ -0,0 +1,32 @@
/*
* 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.management.api;
import java.util.Map;
public interface CommandExecutionArgs
{
/** @return Map of parameter metadata to their values. */
Map<ParameterMetadata, Object> parameters();
/** @return Map of option metadata to their values. */
Map<OptionMetadata, Object> options();
boolean hasParameter(ParameterMetadata param);
boolean hasOption(OptionMetadata option);
}

View File

@ -0,0 +1,56 @@
/*
* 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.management.api;
import java.util.Set;
/**
* Provides execution-time services to a {@link Command}. The context acts as a typed service
* locator so that the API package ({@code management.api}) carries no compile-time dependency
* on tool-specific classes such as {@code NodeProbe} or {@code Output}.
* <p>
* Adapter layers retrieve the concrete services they need:
* <pre>{@code
* NodeProbe probe = context.service(NodeProbe.class);
* Output out = context.service(Output.class);
* }</pre>
* New commands that do not require legacy tool types can request MBean interfaces directly:
* <pre>{@code
* StorageServiceMBean ss = context.service(StorageServiceMBean.class);
* }</pre>
*/
public interface CommandExecutionContext
{
/**
* Returns the service instance of the requested type, or throws
* {@link IllegalArgumentException} if the service is not available in this context.
*
* @param serviceType the class of the desired service
* @param <T> service type
* @return a non-null service instance
* @throws IllegalArgumentException if the service is not available
*/
<T> T service(Class<T> serviceType);
/**
* Retrieves the set of all service types that are available within the current execution context.
* @return a set of classes representing the types of services accessible via this context.
*/
Set<Class<?>> services();
}

View File

@ -0,0 +1,54 @@
/*
* 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.management.api;
import java.util.List;
/**
* CommandMetadata is a protocol-agnostic descriptor of a management command's structure
* and metadata information.
* <p>
* Each {@link Command} exposes a {@code CommandMetadata} that describes the command's name,
* human-readable description, optional aliases, accepted options (named flags/values),
* positional parameters, and subcommands.
* <p>
* This metadata is used by:
* <ul>
* <li>{@link org.apache.cassandra.management.CommandExecutionArgsSerde} to serialize/deserialize
* command arguments to and from JSON or CQL row maps.</li>
* <li>{@link org.apache.cassandra.management.CommandInvokerService} to look up and invoke
* commands over existing MBean interfaces (only intefaces and corresponding implementaions
* are used, JMX might be disabled).;</li>
* <li>CQL syntax {@code COMMAND} to validate and convert user-supplied arguments.</li>
* </ul>
* <p>
* The interface intentionally has no {@code parent()} accessor. Parent-child relationships are
* owned by the {@link org.apache.cassandra.management.CassandraCommandRegistry} (and its
* underlying {@link CommandRegistry} contract) rather than by individual commands, so that
* a command can be registered in different registries.
*/
public interface CommandMetadata
{
String name();
String description();
List<OptionMetadata> options();
List<ParameterMetadata> parameters();
List<CommandMetadata> subcommands();
}

View File

@ -0,0 +1,45 @@
/*
* 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.management.api;
import java.util.Map;
/**
* Registry that knows all of its subcommands and acts as a composite command for hierarchy.
* Allows registering and retrieving commands by name.
* <p>
* For example, nodetool commands structure is like:
* <pre>
* NodetoolCommand (parent)
* setconcurrentcompactors (leaf command, implemented via Command interface)
* compressiondictionary (parent command, implemented via CommandRegistry interface)
* train (leaf)
* list (leaf)
* export (leaf)
* import (leaf)
* decommission (parent)
* abort (leaf)
* ... 100+ more commands
* </pre>
*/
public interface CommandRegistry extends Command<String>
{
<T> Command<T> command(String name);
Iterable<Map.Entry<String, Command<?>>> commands();
}

View File

@ -0,0 +1,30 @@
/*
* 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.management.api;
import java.util.Collection;
/**
* Pluggable component that is responsible for providing a list of commands for management API.
*/
public interface CommandsProvider
{
/** Gets all supported by these provider commands. */
Collection<Command<?>> commands();
}

View File

@ -0,0 +1,23 @@
/*
* 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.management.api;
public interface OptionMetadata extends ArgumentMetadata
{
}

View File

@ -0,0 +1,25 @@
/*
* 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.management.api;
public interface ParameterMetadata extends ArgumentMetadata
{
String COMMAND_POSITIONAL_PARAM_PREFIX = "param";
int index();
}

View File

@ -0,0 +1,134 @@
/*
* 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.management.picocli;
import java.lang.reflect.Field;
import javax.inject.Inject;
import org.apache.cassandra.management.api.Command;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.api.CommandExecutionContext;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.Output;
import org.apache.cassandra.tools.nodetool.AbstractCommand;
import picocli.CommandLine;
public class PicocliCommandAdapter implements Command<Void>
{
private final Class<? extends AbstractCommand> commandClass;
private final CommandMetadata commandMetadata;
public PicocliCommandAdapter(Class<? extends AbstractCommand> commandClass)
{
this.commandClass = commandClass;
this.commandMetadata = PicocliCommandMetadata.from(commandClass);
if (!(commandMetadata instanceof PicocliCommandMetadata))
throw new IllegalStateException("CommandMetadata must be PicocliCommandMetadata for picocli commands");
}
@Override
public CommandMetadata metadata()
{
return commandMetadata;
}
@Override
public Void execute(CommandExecutionArgs arguments, CommandExecutionContext context)
{
Output output = context.service(Output.class);
CommandLine commandLine = new CommandLine(commandClass, new InjectCassandraContext(output));
Object userCommand = commandLine.getCommand();
if (userCommand instanceof AbstractCommand)
((AbstractCommand) userCommand).probe(context.service(NodeProbe.class));
PicocliCommandArgsConverter.toCommand(arguments, userCommand);
// Alternatively, we can convert CommandExecutionArgs to String[] and let picocli handle
// the parsing and execution; however, Cassandra validates a lot of arguments inside the
// command's execution body (historically this was done because of Airline limitations).
//
// Double parsing and validation of arguments is not ideal, and it doesn't make sense
// to rely on picocli either. Verdict: it doesn't make sense to rely on picocli argument
// validation and error handling here.
if (userCommand instanceof Runnable)
{
((Runnable) userCommand).run();
return null;
}
else
{
throw new RuntimeException(String.format("Unsupported command type: %s. " +
"Command class must implement Runnable to be executed.",
userCommand.getClass().getName()));
}
}
private static class InjectCassandraContext implements CommandLine.IFactory
{
private final Output output;
private final CommandLine.IFactory fallback;
public InjectCassandraContext(Output output)
{
this.fallback = CommandLine.defaultFactory();
this.output = output;
}
@Override
public <K> K create(Class<K> cls)
{
try
{
K bean = this.fallback.create(cls);
Class<?> beanClass = bean.getClass();
do
{
Field[] fields = beanClass.getDeclaredFields();
for (Field field : fields)
{
if (!field.isAnnotationPresent(Inject.class))
continue;
field.setAccessible(true);
if (field.getType().equals(Output.class))
{
field.set(bean, output);
}
else
{
throw new RuntimeException("Unsupported injectable field type: " + field.getType() +
" in class " + beanClass.getName() + ". " +
"Only Output is supported for injection.");
}
}
}
while ((beanClass = beanClass.getSuperclass()) != null);
return bean;
}
catch (Exception e)
{
throw new CommandLine.InitializationException("Failed to create instance of " + cls, e);
}
}
}
}

View File

@ -0,0 +1,199 @@
/*
* 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.management.picocli;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.cassandra.management.SimpleCommandExecutionArgs;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.management.api.OptionMetadata;
import org.apache.cassandra.management.api.ParameterMetadata;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
public class PicocliCommandArgsConverter
{
/**
* Extract command arguments from an AbstractCommand instance using picocli reflection.
*
* @param command the command instance to extract arguments from
* @return CommandExecutionArgs containing all options and parameters from the command
*/
public static <T> CommandExecutionArgs fromCommand(T command)
{
CommandMetadata metadata = PicocliCommandMetadata.from(command);
PicocliCommandMetadata picocliMetadata = (PicocliCommandMetadata) metadata;
CommandSpec spec = picocliMetadata.getCommandSpec();
Map<OptionMetadata, Object> options = new LinkedHashMap<>();
Map<ParameterMetadata, Object> parameters = new LinkedHashMap<>();
for (CommandLine.Model.OptionSpec optionSpec : spec.options())
{
if (!optionSpec.isOption())
continue;
Object value = optionSpec.getValue();
if (value == null)
continue;
if (optionSpec.type() == boolean.class || optionSpec.type() == Boolean.class)
{
if (Boolean.TRUE.equals(value))
options.put(new PicocliOptionMetadata(optionSpec), Boolean.TRUE);
}
else
{
options.put(new PicocliOptionMetadata(optionSpec), value);
}
}
for (CommandLine.Model.PositionalParamSpec paramSpec : spec.positionalParameters())
{
if (!paramSpec.isPositional())
continue;
Object value = paramSpec.getValue();
if (value != null)
parameters.put(new PicocliParameterMetadata(paramSpec), value);
}
return new SimpleCommandExecutionArgs(options, parameters);
}
/**
* Populate an AbstractCommand instance with values from CommandExecutionArgs using picocli setters.
*
* @param args the CommandExecutionArgs containing values to set
* @param command the command instance to populate
*/
public static <T> void toCommand(CommandExecutionArgs args, T command)
{
CommandMetadata metadata = PicocliCommandMetadata.from(command);
PicocliCommandMetadata picocliMetadata = (PicocliCommandMetadata) metadata;
CommandSpec spec = picocliMetadata.getCommandSpec();
Map<OptionMetadata, CommandLine.Model.OptionSpec> optionMap = buildOptionSpecMap(spec);
Map<ParameterMetadata, CommandLine.Model.PositionalParamSpec> paramMap = buildParameterSpecMap(spec);
for (Map.Entry<OptionMetadata, Object> entry : args.options().entrySet())
{
OptionMetadata optionMetadata = entry.getKey();
// This value is already converted to the correct type in CommandExecutionArgs.
Object convertedValue = entry.getValue();
if (convertedValue == null)
continue;
CommandLine.Model.OptionSpec optionSpec = optionMap.get(optionMetadata);
if (optionSpec == null)
{
optionSpec = findOptionSpecByName(spec, optionMetadata.names());
if (optionSpec == null)
throw new IllegalArgumentException("Option not found in command spec: " + Arrays.toString(optionMetadata.names()));
}
try
{
optionSpec.setter().set(convertedValue);
}
catch (Exception e)
{
throw new RuntimeException("Failed to set option " + optionMetadata.names()[0] +
" with value " + convertedValue, e);
}
}
for (Map.Entry<ParameterMetadata, Object> entry : args.parameters().entrySet())
{
ParameterMetadata paramMetadata = entry.getKey();
Object value = entry.getValue();
if (value == null)
continue;
CommandLine.Model.PositionalParamSpec paramSpec = paramMap.get(paramMetadata);
if (paramSpec == null)
{
paramSpec = findParameterSpecByIndex(spec, paramMetadata.index());
if (paramSpec == null)
throw new IllegalArgumentException("Parameter not found in command spec at index: " + paramMetadata.index());
}
try
{
paramSpec.setter().set(value);
}
catch (Exception e)
{
throw new RuntimeException("Failed to set parameter at index " + paramMetadata.index() +
" with value " + value, e);
}
}
}
private static Map<OptionMetadata, CommandLine.Model.OptionSpec> buildOptionSpecMap(CommandSpec spec)
{
Map<OptionMetadata, CommandLine.Model.OptionSpec> map = new LinkedHashMap<>();
for (CommandLine.Model.OptionSpec optionSpec : spec.options())
{
if (optionSpec.isOption())
map.put(new PicocliOptionMetadata(optionSpec), optionSpec);
}
return map;
}
private static Map<ParameterMetadata, CommandLine.Model.PositionalParamSpec> buildParameterSpecMap(CommandSpec spec)
{
Map<ParameterMetadata, CommandLine.Model.PositionalParamSpec> map = new LinkedHashMap<>();
for (CommandLine.Model.PositionalParamSpec paramSpec : spec.positionalParameters())
{
if (paramSpec.isPositional())
map.put(new PicocliParameterMetadata(paramSpec), paramSpec);
}
return map;
}
private static CommandLine.Model.OptionSpec findOptionSpecByName(CommandSpec spec, String[] names)
{
for (CommandLine.Model.OptionSpec optionSpec : spec.options())
{
for (String name : names)
{
if (java.util.Arrays.asList(optionSpec.names()).contains(name))
return optionSpec;
}
}
return null;
}
private static CommandLine.Model.PositionalParamSpec findParameterSpecByIndex(CommandSpec spec, int index)
{
for (CommandLine.Model.PositionalParamSpec paramSpec : spec.positionalParameters())
{
CommandLine.Range indexRange = paramSpec.index();
if (index >= indexRange.min() && index <= indexRange.max())
return paramSpec;
}
return null;
}
}

View File

@ -0,0 +1,132 @@
/*
* 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.management.picocli;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.management.api.OptionMetadata;
import org.apache.cassandra.management.api.ParameterMetadata;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
/**
* Implementation of CommandMetadata that extracts metadata from picocli CommandSpec.
*/
public class PicocliCommandMetadata implements CommandMetadata
{
private final CommandSpec commandSpec;
public PicocliCommandMetadata(CommandSpec commandSpec)
{
this.commandSpec = commandSpec;
}
/**
* Create CommandMetadata from a command class.
*/
public static CommandMetadata from(Class<?> commandClass)
{
CommandSpec spec = CommandSpec.forAnnotatedObject(commandClass);
return new PicocliCommandMetadata(spec);
}
/**
* Create CommandMetadata from a command instance.
*/
public static CommandMetadata from(Object commandInstance)
{
CommandSpec spec = CommandSpec.forAnnotatedObject(commandInstance);
return new PicocliCommandMetadata(spec);
}
@Override
public String name()
{
return commandSpec.name();
}
@Override
public String description()
{
String[] description = commandSpec.usageMessage().description();
if (description == null || description.length == 0)
return "";
return String.join("\n", description);
}
@Override
public List<OptionMetadata> options()
{
List<OptionMetadata> options = new ArrayList<>();
for (CommandLine.Model.OptionSpec option : commandSpec.options())
{
if (option.isOption())
options.add(new PicocliOptionMetadata(option));
}
return options;
}
@Override
public List<ParameterMetadata> parameters()
{
List<ParameterMetadata> parameters = new ArrayList<>();
for (CommandLine.Model.PositionalParamSpec positional : commandSpec.positionalParameters())
{
if (positional.isPositional())
parameters.add(new PicocliParameterMetadata(positional));
}
return parameters;
}
@Override
public List<CommandMetadata> subcommands()
{
return commandSpec.subcommands().values().stream()
.map(subcommand -> new PicocliCommandMetadata(subcommand.getCommandSpec()))
.collect(Collectors.toList());
}
/**
* Get the underlying CommandSpec for advanced usage.
*/
public CommandSpec getCommandSpec()
{
return commandSpec;
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof PicocliCommandMetadata)) return false;
PicocliCommandMetadata that = (PicocliCommandMetadata) o;
return Objects.equals(commandSpec, that.commandSpec);
}
@Override
public int hashCode()
{
return Objects.hashCode(commandSpec);
}
}

View File

@ -0,0 +1,137 @@
/*
* 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.management.picocli;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.cassandra.management.api.Command;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.api.CommandExecutionContext;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.management.api.CommandRegistry;
import org.apache.cassandra.tools.nodetool.AbstractCommand;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
/**
* Adapter that wraps a picocli command with subcommands and implements CommandRegistry
* using the Composite pattern. This allows hierarchical picocli commands to be used
* as CommandRegistry instances.
* <p>
* For example, {@code CompressionDictionaryCommandGroup} has subcommands (train, list, export, import),
* so it would be wrapped by this adapter to provide a CommandRegistry interface.
* <p>
* The adapter recursively adapts subcommands:
* <ul>
* <li>If a subcommand has subcommands -> creates another {@code PicocliCommandRegistryAdapter}</li>
* <li>If a subcommand is a leaf -> creates a {@code PicocliCommandAdapter}</li>
* </ul>
*/
public class PicocliCommandRegistryAdapter implements CommandRegistry
{
private final CommandSpec commandSpec;
private final CommandMetadata commandMetadata;
private final Map<String, Command<?>> subcommandMap = new ConcurrentHashMap<>();
/**
* Create an adapter for a picocli command class that has subcommands.
* @param commandClass the picocli command class (must have subcommands)
*/
public PicocliCommandRegistryAdapter(Class<? extends AbstractCommand> commandClass)
{
this.commandSpec = CommandSpec.forAnnotatedObject(commandClass);
this.commandMetadata = new PicocliCommandMetadata(commandSpec);
if (commandSpec.subcommands().isEmpty())
{
throw new IllegalArgumentException(
String.format("Command class %s does not have subcommands. " +
"Use PicocliCommandAdapter for leaf commands.",
commandClass.getName()));
}
for (Map.Entry<String, CommandLine> e : commandSpec.subcommands().entrySet())
adaptSubcommands(e.getKey(), e.getValue());
}
/**
* Recursively adapt all subcommands from the picocli command. Uses a Composite pattern:
* subcommands with subcommands become registries, leaf subcommands become command adapters.
*/
private void adaptSubcommands(String commandName, CommandLine command)
{
CommandSpec subcommandSpec = command.getCommandSpec();
Command<?> adaptedCommand;
if (!subcommandSpec.subcommands().isEmpty())
{
adaptedCommand = new PicocliCommandRegistryAdapter(command.getCommand());
}
else
{
Class<?> subcommandClass = command.getCommand().getClass();
if (!AbstractCommand.class.isAssignableFrom(subcommandClass))
{
throw new IllegalArgumentException(
String.format("Subcommand class %s is not an AbstractCommand and cannot be adapted. " +
"Only AbstractCommand subclasses are supported for leaf commands.",
subcommandClass.getName()));
}
adaptedCommand = new PicocliCommandAdapter((Class<? extends AbstractCommand>) subcommandClass);
}
subcommandMap.put(commandName, adaptedCommand);
for (String alias : subcommandSpec.aliases())
subcommandMap.putIfAbsent(alias, adaptedCommand);
}
@Override
public CommandMetadata metadata()
{
return commandMetadata;
}
@Override
public String execute(CommandExecutionArgs arguments, CommandExecutionContext context)
{
// CommandRegistry is not directly executable: routing happens at the invoker/CQL layer.
throw new UnsupportedOperationException(
String.format("CommandRegistry '%s' is not directly executable. " +
"Specify a subcommand. Available commands: %s",
name(),
String.join(", ", subcommandMap.keySet())));
}
@Override
public Command<?> command(String name)
{
return subcommandMap.get(name);
}
@Override
public Iterable<Map.Entry<String, Command<?>>> commands()
{
return subcommandMap.entrySet();
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.management.picocli;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.cassandra.management.api.Command;
import org.apache.cassandra.management.api.CommandsProvider;
import org.apache.cassandra.tools.nodetool.AbstractCommand;
import org.apache.cassandra.tools.nodetool.NodetoolCommand;
import picocli.CommandLine;
public class PicocliCommandsProvider implements CommandsProvider
{
@Override
public Collection<Command<?>> commands()
{
CommandLine commandLine = new CommandLine(NodetoolCommand.class);
List<Command<?>> commands = new ArrayList<>();
commandLine.getSubcommands().forEach((name, subcommandLine) -> {
if (!subcommandLine.getCommandSpec().subcommands().isEmpty())
{
@SuppressWarnings("unchecked")
Class<? extends AbstractCommand> abstractCommandClass =
(Class<? extends AbstractCommand>) subcommandLine.getCommand().getClass();
commands.add(new PicocliCommandRegistryAdapter(abstractCommandClass));
}
else
{
Class<?> commandClass = subcommandLine.getCommand().getClass();
if (AbstractCommand.class.isAssignableFrom(commandClass))
{
@SuppressWarnings("unchecked")
Class<? extends AbstractCommand> abstractCommandClass =
(Class<? extends AbstractCommand>) commandClass;
commands.add(new PicocliCommandAdapter(abstractCommandClass));
}
}
});
return commands;
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.management.picocli;
import org.apache.cassandra.management.api.CommandMetadata;
import org.apache.cassandra.tools.nodetool.AbstractCommand;
/**
* Utility class for extracting command metadata from picocli-annotated command classes.
*/
public class PicocliMetadataExtractor
{
/**
* Extract CommandMetadata from an AbstractCommand class.
*
* @param commandClass The command class annotated with picocli @Command
* @return CommandMetadata extracted from the command class
*/
public static CommandMetadata extract(Class<? extends AbstractCommand> commandClass)
{
return PicocliCommandMetadata.from(commandClass);
}
/**
* Extract CommandMetadata from a command instance.
*
* @param commandInstance The command instance
* @return CommandMetadata extracted from the command instance
*/
public static CommandMetadata extract(Object commandInstance)
{
if (commandInstance instanceof AbstractCommand)
return PicocliCommandMetadata.from(commandInstance);
throw new IllegalArgumentException("Unsupported command instance type: " + commandInstance.getClass().getName());
}
}

View File

@ -0,0 +1,167 @@
/*
* 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.management.picocli;
import java.util.Arrays;
import java.util.Objects;
import com.google.common.base.Strings;
import org.apache.cassandra.management.api.OptionMetadata;
import picocli.CommandLine;
import picocli.CommandLine.Model.OptionSpec;
import static org.apache.cassandra.management.ManagementUtils.elementType;
import static org.apache.cassandra.management.ManagementUtils.stripAngleBrackets;
/**
* Implementation of OptionMetadata that extracts metadata from picocli OptionSpec.
*/
public class PicocliOptionMetadata implements OptionMetadata
{
private final OptionSpec optionSpec;
public PicocliOptionMetadata(OptionSpec optionSpec)
{
this.optionSpec = optionSpec;
}
@Override
public String[] names()
{
return optionSpec.names();
}
@Override
public String description()
{
String[] description = optionSpec.description();
if (description == null || description.length == 0)
return "";
return String.join("\n", description);
}
@Override
public Class<?> type()
{
return optionSpec.type();
}
@Override
public String defaultValue()
{
Object defaultValue = optionSpec.defaultValue();
if (defaultValue == null)
{
if (optionSpec.type() == boolean.class || optionSpec.type() == Boolean.class)
return "false";
return "";
}
return defaultValue.toString();
}
@Override
public boolean required()
{
return optionSpec.required();
}
@Override
public String arity()
{
CommandLine.Range arity = optionSpec.arity();
int min = arity.min();
int max = arity.max();
if (min == 0 && max == 0)
return "0";
if (min == max)
return String.valueOf(min);
else if (max == Integer.MAX_VALUE)
return min + "..*";
else
return min + ".." + max;
}
@Override
public String paramLabel()
{
String paramLabel = optionSpec.paramLabel();
Object userObject = optionSpec.userObject();
return Strings.isNullOrEmpty(paramLabel) ?
((java.lang.reflect.Field) userObject).getName() :
stripAngleBrackets(paramLabel);
}
/**
* Check if this is a boolean flag option.
*/
public boolean isFlag()
{
return optionSpec.arity().max() == 0;
}
/**
* Get the underlying OptionSpec for advanced usage.
*/
public OptionSpec getOptionSpec()
{
return optionSpec;
}
public Object convertValue(Object value) throws Exception
{
TypeConverter<?> customConverter = typeConverter() == null ? null : typeConverter()[0];
return customConverter == null ?
TypeConverterRegistry.convertValueBasic(value, type(), elementType(optionSpec)) :
customConverter.convert(value.toString());
}
private TypeConverter<?>[] typeConverter()
{
return TypeConverter.createFrom(optionSpec);
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof PicocliOptionMetadata)) return false;
PicocliOptionMetadata that = (PicocliOptionMetadata) o;
return Objects.equals(optionSpec, that.optionSpec);
}
@Override
public int hashCode()
{
return Objects.hashCode(optionSpec);
}
@Override
public String toString()
{
return "PicocliOptionMetadata{" +
"names=" + Arrays.toString(names()) +
'}';
}
}

View File

@ -0,0 +1,158 @@
/*
* 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.management.picocli;
import java.util.Arrays;
import java.util.Objects;
import org.apache.cassandra.management.api.ParameterMetadata;
import picocli.CommandLine;
import picocli.CommandLine.Model.PositionalParamSpec;
import static org.apache.cassandra.management.ManagementUtils.elementType;
import static org.apache.cassandra.management.ManagementUtils.stripAngleBrackets;
/**
* Implementation of ParameterMetadata that extracts metadata from picocli PositionalParamSpec.
*/
public class PicocliParameterMetadata implements ParameterMetadata
{
private final PositionalParamSpec positionalParamSpec;
public PicocliParameterMetadata(PositionalParamSpec positionalParamSpec)
{
this.positionalParamSpec = positionalParamSpec;
}
@Override
public String[] names()
{
String name;
String paramLabel = positionalParamSpec.paramLabel();
Object userObject = positionalParamSpec.userObject();
if (paramLabel != null && !paramLabel.isEmpty())
name = stripAngleBrackets(paramLabel);
else if (userObject instanceof java.lang.reflect.Field)
name = ((java.lang.reflect.Field) userObject).getName();
else
name = COMMAND_POSITIONAL_PARAM_PREFIX + index();
return new String[] { name };
}
@Override
public String paramLabel()
{
return stripAngleBrackets(positionalParamSpec.paramLabel());
}
@Override
public String description()
{
String[] description = positionalParamSpec.description();
if (description == null || description.length == 0)
return "";
return String.join("\n", description);
}
@Override
public Class<?> type()
{
return positionalParamSpec.type();
}
@Override
public String defaultValue()
{
return "";
}
@Override
public int index()
{
return positionalParamSpec.index().min();
}
@Override
public boolean required()
{
return positionalParamSpec.arity().min() > 0;
}
@Override
public String arity()
{
CommandLine.Range arity = positionalParamSpec.arity();
int min = arity.min();
int max = arity.max();
if (min == max)
return String.valueOf(min);
else if (max == Integer.MAX_VALUE)
return min + "..*";
else
return min + ".." + max;
}
/**
* Get the underlying PositionalParamSpec for advanced usage.
*/
public PositionalParamSpec getPositionalParamSpec()
{
return positionalParamSpec;
}
public Object convertValue(Object value) throws Exception
{
TypeConverter<?> customConverter = typeConverter() == null ? null : typeConverter()[0];
return customConverter == null ?
TypeConverterRegistry.convertValueBasic(value, type(), elementType(positionalParamSpec)) :
customConverter.convert(value.toString());
}
private TypeConverter<?>[] typeConverter()
{
return TypeConverter.createFrom(positionalParamSpec);
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof PicocliParameterMetadata)) return false;
PicocliParameterMetadata that = (PicocliParameterMetadata) o;
return Objects.equals(positionalParamSpec, that.positionalParamSpec);
}
@Override
public int hashCode()
{
return Objects.hashCode(positionalParamSpec);
}
@Override
public String toString()
{
return "PicocliParameterMetadata{" +
"names=" + Arrays.toString(names()) +
'}';
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.management.picocli;
import java.util.Arrays;
import picocli.CommandLine;
public interface TypeConverter<R>
{
R convert(String value) throws Exception;
static TypeConverter<?>[] createFrom(CommandLine.Model.ArgSpec argSpec)
{
CommandLine.ITypeConverter<?>[] converters = argSpec.converters();
return converters == null || converters.length == 0 ?
null :
Arrays.stream(converters)
.map(c -> (TypeConverter<Object>) c::convert)
.toArray(TypeConverter<?>[]::new);
}
}

View File

@ -0,0 +1,177 @@
/*
* 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.management.picocli;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
public class TypeConverterRegistry
{
private static final Map<Class<?>, TypeConverter<?>> converters = new LinkedHashMap<>();
static
{
register(boolean.class, (TypeConverter<Boolean>) Boolean::parseBoolean);
register(int.class, (TypeConverter<Integer>) Integer::parseInt);
register(long.class, (TypeConverter<Long>) Long::parseLong);
register(double.class, (TypeConverter<Double>) Double::parseDouble);
register(float.class, (TypeConverter<Float>) Float::parseFloat);
register(byte.class, (TypeConverter<Byte>) Byte::parseByte);
register(short.class, (TypeConverter<Short>) Short::parseShort);
register(char.class, value -> {
if (value.length() != 1)
throw new IllegalArgumentException("Cannot convert to char: " + value);
return value.charAt(0);
});
register(Boolean.class, (TypeConverter<Boolean>) Boolean::parseBoolean);
register(Integer.class, (TypeConverter<Integer>) Integer::parseInt);
register(Long.class, (TypeConverter<Long>) Long::parseLong);
register(Double.class, (TypeConverter<Double>) Double::parseDouble);
register(Float.class, (TypeConverter<Float>) Float::parseFloat);
register(Byte.class, (TypeConverter<Byte>) Byte::parseByte);
register(Short.class, (TypeConverter<Short>) Short::parseShort);
register(String.class, (TypeConverter<String>) value -> value);
register(Character.class, value -> {
if (value.length() != 1)
throw new IllegalArgumentException("Cannot convert to Character: " + value);
return value.charAt(0);
});
register(Object.class, value -> value);
}
private static void register(Class<?> type, TypeConverter<?> converter)
{
converters.put(type, converter);
}
public static <T> Object convertValueBasic(Object value, Class<T> targetType) throws Exception
{
return convertValueBasic(value, targetType, null);
}
public static <T> Object convertValueBasic(Object value, Class<T> targetType, Class<?> elementType) throws Exception
{
if (value == null)
return null;
// Arrays and collections must be handled before the assignable short-circuit below: a
// List<String> is already assignable to List, but its elements still need per-element
// conversion when the declared element type is not String.
if (targetType.isArray() || Collection.class.isAssignableFrom(targetType))
return convertToArrayOrCollection(value, targetType, elementType);
if (targetType.isAssignableFrom(value.getClass()))
return value;
String stringValue = value.toString();
if (isBasicType(targetType))
{
TypeConverter<?> converter = converters.get(targetType);
if (converter == null)
throw new IllegalStateException(String.format("No converter registered for basic type: %s",
targetType.getName()));
return converter.convert(stringValue);
}
if (targetType.isEnum())
return getEnumTypeConverter(targetType).convert(stringValue);
throw new IllegalArgumentException(String.format("No converter available for type: %s.", targetType.getName()));
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static <T> TypeConverter<T> getEnumTypeConverter(Class<T> type)
{
return value -> {
for (T constant : type.getEnumConstants())
if (((Enum<?>) constant).name().equalsIgnoreCase(value))
return constant;
throw new IllegalArgumentException("No enum constant " + type.getCanonicalName() + '.' + value);
};
}
private static Object convertToArrayOrCollection(Object value, Class<?> targetType, Class<?> elementType) throws Exception
{
Collection<?> sourceCollection;
if (value instanceof Collection)
sourceCollection = (Collection<?>) value;
else if (value.getClass().isArray())
{
int length = Array.getLength(value);
List<Object> list = new ArrayList<>(length);
for (int i = 0; i < length; i++)
list.add(Array.get(value, i));
sourceCollection = list;
}
else
throw new IllegalArgumentException("Cannot convert " + value.getClass() + " to " + targetType);
if (targetType.isArray())
{
Class<?> componentType = targetType.getComponentType();
Object targetArray = Array.newInstance(componentType, sourceCollection.size());
int index = 0;
for (Object item : sourceCollection)
{
Object convertedItem = convertValueBasic(item, componentType);
Array.set(targetArray, index++, convertedItem);
}
return targetArray;
}
Collection<Object> targetCollection;
if (targetType == List.class || List.class.isAssignableFrom(targetType))
targetCollection = new ArrayList<>();
else if (SortedSet.class.isAssignableFrom(targetType))
targetCollection = new TreeSet<>();
else if (targetType == Set.class || Set.class.isAssignableFrom(targetType))
targetCollection = new LinkedHashSet<>();
else
targetCollection = new ArrayList<>();
if (elementType == null)
targetCollection.addAll(sourceCollection);
else
for (Object item : sourceCollection)
targetCollection.add(convertValueBasic(item, elementType));
return targetCollection;
}
private static boolean isBasicType(Class<?> type)
{
return type == String.class ||
type == boolean.class || type == Boolean.class ||
type == int.class || type == Integer.class ||
type == long.class || type == Long.class ||
type == double.class || type == Double.class ||
type == float.class || type == Float.class ||
type == byte.class || type == Byte.class ||
type == short.class || type == Short.class ||
type == char.class || type == Character.class;
}
}

View File

@ -80,6 +80,7 @@ import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Locator;
import org.apache.cassandra.management.CommandInvokerService;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.metrics.DefaultNameFactory;
import org.apache.cassandra.net.StartupClusterConnectivityChecker;
@ -99,6 +100,7 @@ import org.apache.cassandra.tcm.RegistrationStatus;
import org.apache.cassandra.tcm.Startup;
import org.apache.cassandra.tcm.membership.NodeId;
import org.apache.cassandra.tcm.membership.NodeState;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.JMXServerUtils;
import org.apache.cassandra.utils.JVMStabilityInspector;
@ -218,6 +220,7 @@ public class CassandraDaemon
static final CassandraDaemon instance = new CassandraDaemon();
private volatile NativeTransportManagementService nativeTransportManagementService;
private volatile NativeTransportService nativeTransportService;
private JMXConnectorServer jmxServer;
@ -397,6 +400,9 @@ public class CassandraDaemon
// re-enable auto-compaction after replay, so correct disk boundaries are used
enableAutoCompaction(Schema.instance.getKeyspaces());
// Initialize command service (after JMX, before StorageService.initServer)
CommandInvokerService.instance.start();
// start server internals
StorageService.instance.registerDaemon(this);
try
@ -668,6 +674,9 @@ public class CassandraDaemon
public synchronized void initializeClientTransports()
{
if (nativeTransportManagementService == null)
nativeTransportManagementService = new NativeTransportManagementService();
// Native transport
if (nativeTransportService == null)
nativeTransportService = new NativeTransportService();
@ -755,6 +764,9 @@ public class CassandraDaemon
Set<InetAddressAndPort> peers = new HashSet<>(ClusterMetadata.current().directory.allJoinedEndpoints());
connectivityChecker.execute(peers, ep -> locator.location(ep).datacenter);
// start management transports first.
startManagementTransport();
// check to see if transports may start else return without starting. This is needed when in survey mode or
// when bootstrap has not completed.
try
@ -783,6 +795,16 @@ public class CassandraDaemon
logger.info("Not starting native transport as requested. Use JMX (StorageService->startNativeTransport()) or nodetool (enablebinary) to start it");
}
private void startManagementTransport()
{
if (NativeTransportManagementService.enabled())
{
if (nativeTransportManagementService == null)
throw new IllegalStateException("setup() must be called first for CassandraDaemon");
nativeTransportManagementService.start();
}
}
/**
* Stop the daemon, ideally in an idempotent manner.
*
@ -794,6 +816,7 @@ public class CassandraDaemon
// jsvc takes care of taking the rest down
logger.info("Cassandra shutting down...");
destroyClientTransports();
CommandInvokerService.instance.stop();
StorageService.instance.setRpcReady(false);
if (jmxServer != null)
@ -815,6 +838,9 @@ public class CassandraDaemon
stopNativeTransport();
if (nativeTransportService != null)
nativeTransportService.destroy();
if (nativeTransportManagementService != null)
nativeTransportManagementService.destroy();
Dispatcher.shutdown();
}
/**
@ -995,12 +1021,18 @@ public class CassandraDaemon
public void clearConnectionHistory()
{
nativeTransportService.clearConnectionHistory();
if (nativeTransportService != null)
nativeTransportService.clearConnectionHistory();
if (nativeTransportManagementService != null)
nativeTransportManagementService.clearConnectionHistory();
}
public void disconnectUser(Predicate<AuthenticatedUser> userPredicate)
{
nativeTransportService.disconnect(userPredicate);
if (nativeTransportService != null)
nativeTransportService.disconnect(userPredicate);
if (nativeTransportManagementService != null)
nativeTransportManagementService.disconnect(userPredicate);
}
private void exitOrFail(int code, String message)

View File

@ -147,6 +147,9 @@ public class ClientState
// Driver String for the client
private volatile String driverName;
private volatile String driverVersion;
// Whether this client is connected via the management native transport port
private volatile boolean isManagement;
// Options provided by the client
private volatile Map<String,String> clientOptions;
@ -212,6 +215,7 @@ public class ClientState
this.driverName = source.driverName;
this.driverVersion = source.driverVersion;
this.clientOptions = source.clientOptions;
this.isManagement = source.isManagement;
}
/**
@ -356,7 +360,17 @@ public class ClientState
{
this.driverVersion = driverVersion;
}
public boolean isManagement()
{
return isManagement;
}
public void setManagement(boolean isManagement)
{
this.isManagement = isManagement;
}
public void setClientOptions(Map<String,String> clientOptions)
{
this.clientOptions = ImmutableMap.copyOf(clientOptions);

View File

@ -0,0 +1,162 @@
/*
* 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.service;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.transport.Server;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.Version;
import static org.apache.cassandra.config.CassandraRelevantProperties.START_NATIVE_MANAGEMENT_TRANSPORT;
import static org.apache.cassandra.service.NativeTransportService.useEpoll;
/**
* Manages the native management transport server on port {@code 11211}.
* This service is independent of NativeTransportService and can only be
* enabled/disabled via configuration or system property (not at runtime).
*/
public class NativeTransportManagementService implements CassandraDaemon.Server
{
private static final Logger logger = LoggerFactory.getLogger(NativeTransportManagementService.class);
private Server server = null;
private EventLoopGroup workerGroup;
private volatile boolean initialized = false;
@VisibleForTesting
synchronized void initialize()
{
if (initialized)
return;
if (useEpoll())
{
workerGroup = new EpollEventLoopGroup();
logger.info("Management transport Netty using native Epoll event loop");
}
else
{
workerGroup = new NioEventLoopGroup();
logger.info("Management transport Netty using Java NIO event loop");
}
logger.info("Management transport Netty Version: {}", Version.identify().entrySet());
int managementPort = DatabaseDescriptor.getNativeTransportManagementPort();
InetAddress addr = DatabaseDescriptor.getRpcManagementAddress();
EncryptionOptions.TlsEncryptionPolicy encryptionPolicy = DatabaseDescriptor.getNativeProtocolEncryptionOptions()
.tlsEncryptionPolicy();
server = new Server.Builder()
.withEventLoopGroup(workerGroup)
.withHost(addr)
.withTlsEncryptionPolicy(encryptionPolicy)
.withPort(managementPort)
.withManagementConnectionFlag(true)
.build();
initialized = true;
}
/**
* @return whether the management transport should start: the
* {@code cassandra.start_native_management_transport} system property wins when set,
* otherwise the {@code start_native_transport_management} yaml setting decides.
*/
public static boolean enabled()
{
if (START_NATIVE_MANAGEMENT_TRANSPORT.getString() != null)
return START_NATIVE_MANAGEMENT_TRANSPORT.getBoolean();
return DatabaseDescriptor.startNativeTransportManagement();
}
public void start()
{
if (!enabled())
{
logger.info("Management transport is disabled via configuration and will not start.");
return;
}
initialize();
server.start();
logger.info("Management transport started on port: {}", server.socket.getPort());
}
public void stop()
{
if (server != null)
server.stop(false);
}
public void destroy()
{
stop();
if (server == null)
return;
server = null;
if (workerGroup != null)
workerGroup.shutdownGracefully(3, 5, TimeUnit.SECONDS).awaitUninterruptibly();
initialized = false;
}
/** @return true if the management transport server is running. */
public boolean isRunning()
{
return server != null && server.isRunning();
}
/** Clears connection history for this service's server. */
public void clearConnectionHistory()
{
if (server != null)
server.clearConnectionHistory();
}
/** Disconnects users matching the predicate from this service's server. */
public void disconnect(Predicate<AuthenticatedUser> userPredicate)
{
if (server != null)
server.disconnect(userPredicate);
}
@VisibleForTesting
public EventLoopGroup getWorkerGroup()
{
return workerGroup;
}
}

View File

@ -30,7 +30,6 @@ import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.metrics.ClientMetrics;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.transport.Server;
import org.apache.cassandra.utils.NativeLibrary;
@ -45,7 +44,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.NATIVE_EPO
/**
* Handles native transport server lifecycle and associated resources. Lazily initialized.
*/
public class NativeTransportService
public class NativeTransportService implements CassandraDaemon.Server
{
private static final Logger logger = LoggerFactory.getLogger(NativeTransportService.class);
@ -126,8 +125,6 @@ public class NativeTransportService
// shutdown executors used by netty for native transport server
if (workerGroup != null)
workerGroup.shutdownGracefully(3, 5, TimeUnit.SECONDS).awaitUninterruptibly();
Dispatcher.shutdown();
}
/**

View File

@ -32,11 +32,11 @@ class NodeProbeFactory implements INodeProbeFactory
public NodeProbe create(String host, int port) throws IOException
{
return new NodeProbe(host, port);
return new NodeProbe(new RemoteJmxMBeanAccessor(host, port));
}
public NodeProbe create(String host, int port, String username, String password) throws IOException
{
return new NodeProbe(host, port, username, password);
return new NodeProbe(new RemoteJmxMBeanAccessor(host, port, username, password));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -39,9 +39,13 @@ import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileWriter;
import org.apache.cassandra.tools.nodetool.CqlConnect;
import org.apache.cassandra.tools.nodetool.JmxConnect;
import org.apache.cassandra.tools.nodetool.NodetoolCommand;
import org.apache.cassandra.tools.nodetool.layout.CassandraCliHelpLayout;
import org.apache.cassandra.tools.nodetool.strategy.CommandExecutionStrategy;
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
import org.apache.cassandra.tools.nodetool.strategy.ProtocolAwareExecutionStrategy;
import org.apache.cassandra.utils.FBUtilities;
import picocli.CommandLine;
@ -104,7 +108,7 @@ public class NodeTool
commandLine.setErr(new PrintWriter(output.err, true));
configureCliLayout(commandLine);
commandLine.setExecutionStrategy(JmxConnect::executionStrategy)
commandLine.setExecutionStrategy(ProtocolAwareExecutionStrategy::executionStrategy)
.setExecutionExceptionHandler((ex, c, arg) -> {
// Used for backward compatibility, some commands are validated when a command is run.
if (ex instanceof IllegalArgumentException |
@ -114,7 +118,25 @@ public class NodeTool
return 1;
}
err(Throwables.getRootCause(ex));
NodetoolConnectionException connectFailure = Throwables.getCausalChain(ex).stream()
.filter(NodetoolConnectionException.class::isInstance)
.map(NodetoolConnectionException.class::cast)
.findFirst().orElse(null);
if (connectFailure != null)
{
output.err.println("nodetool: " + connectFailure.getMessage());
return 1;
}
// CASSANDRA-11537 friendly error message when server is not ready
Throwable root = Throwables.getRootCause(ex);
if (root instanceof InstanceNotFoundException)
{
badUse(new IllegalArgumentException("Server is not initialized yet, cannot run nodetool."));
return 1;
}
err(root);
return 2;
})
.setParameterExceptionHandler((ex, arg) -> {
@ -195,8 +217,18 @@ public class NodeTool
public static CommandLine createCommandLine(CommandLine.IFactory factory) throws Exception
{
return new CommandLine(new NodetoolCommand(), factory)
.addMixin(JmxConnect.MIXIN_KEY, factory.create(JmxConnect.class));
CommandLine commandLine = new CommandLine(new NodetoolCommand(), factory);
CommandExecutionStrategy.Type strategyType = ProtocolAwareExecutionStrategy.getExecutionStrategyTypeFromEnvAndSys();
switch (strategyType)
{
case CQL:
return commandLine.addMixin(strategyType.toString(), factory.create(CqlConnect.class));
case STATIC_MBEAN:
case COMMAND_MBEAN:
return commandLine.addMixin(strategyType.toString(), factory.create(JmxConnect.class));
default:
throw new IllegalStateException("Unknown execution strategy: " + strategyType);
}
}
private static void configureCliLayout(CommandLine commandLine)
@ -229,10 +261,6 @@ public class NodeTool
protected void err(Throwable e)
{
// CASSANDRA-11537: friendly error message when server is not ready
if (e instanceof InstanceNotFoundException)
throw new IllegalArgumentException("Server is not initialized yet, cannot run nodetool.");
output.err.println("error: " + e.getMessage());
output.err.println("-- StackTrace --");
output.err.println(getStackTraceAsString(e));

View File

@ -33,11 +33,21 @@ public class Output
this.err = err;
}
public void printInfo(String msg)
{
out.println(msg);
}
public void printInfo(String msg, Object... args)
{
out.printf(msg, args);
}
public void printError(String msg)
{
err.println(msg);
}
public void printError(String msg, Object... args)
{
err.printf(msg, args);

View File

@ -0,0 +1,479 @@
/*
* 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.tools;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.RuntimeMXBean;
import java.rmi.ConnectException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMISocketFactory;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import com.google.common.base.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.audit.AuditLogManagerMBean;
import org.apache.cassandra.auth.AuthCache;
import org.apache.cassandra.auth.CIDRGroupsMappingManager;
import org.apache.cassandra.auth.CIDRGroupsMappingManagerMBean;
import org.apache.cassandra.auth.CIDRPermissionsManager;
import org.apache.cassandra.auth.CIDRPermissionsManagerMBean;
import org.apache.cassandra.auth.NetworkPermissionsCache;
import org.apache.cassandra.auth.NetworkPermissionsCacheMBean;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.auth.PermissionsCache;
import org.apache.cassandra.auth.PermissionsCacheMBean;
import org.apache.cassandra.auth.RolesCache;
import org.apache.cassandra.auth.RolesCacheMBean;
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
import org.apache.cassandra.batchlog.BatchlogManager;
import org.apache.cassandra.batchlog.BatchlogManagerMBean;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.guardrails.GuardrailsMBean;
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable;
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTableMBean;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.gms.FailureDetectorMBean;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.GossiperMBean;
import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.hints.HintsServiceMBean;
import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
import org.apache.cassandra.locator.LocationInfoMBean;
import org.apache.cassandra.management.MBeanAccessor;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.MessagingServiceMBean;
import org.apache.cassandra.profiler.AsyncProfilerMBean;
import org.apache.cassandra.service.ActiveRepairServiceMBean;
import org.apache.cassandra.service.AutoRepairService;
import org.apache.cassandra.service.AutoRepairServiceMBean;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.service.CacheServiceMBean;
import org.apache.cassandra.service.GCInspector;
import org.apache.cassandra.service.GCInspectorMXBean;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageProxyMBean;
import org.apache.cassandra.service.StorageServiceMBean;
import org.apache.cassandra.service.accord.AccordOperations;
import org.apache.cassandra.service.accord.AccordOperationsMBean;
import org.apache.cassandra.service.snapshot.SnapshotManagerMBean;
import org.apache.cassandra.streaming.StreamManagerMBean;
import org.apache.cassandra.tcm.CMSOperations;
import org.apache.cassandra.tcm.CMSOperationsMBean;
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
import static org.apache.cassandra.config.CassandraRelevantProperties.SSL_ENABLE;
public class RemoteJmxMBeanAccessor implements MBeanAccessor
{
private final Map<Class<?>, Object> clazzMBanRegistry = new HashMap<>();
private final Map<String, Object> namedMBeanRegistry = new ConcurrentHashMap<>();
public static final int defaultPort = 7199;
private static final Logger logger = LoggerFactory.getLogger(RemoteJmxMBeanAccessor.class);
private static final String fmtUrl = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi";
final String host;
final int port;
private String username;
private String password;
protected JMXConnector jmxc;
protected MBeanServerConnection mbeanServerConn;
private volatile boolean connected = false;
/**
* Creates a NodeProbe using the specified JMX host, port, username, and password.
*
* @param host hostname or IP address of the JMX agent
* @param port TCP port of the remote JMX agent
*/
public RemoteJmxMBeanAccessor(String host, int port, String username, String password)
{
assert username != null && !username.isEmpty() && password != null && !password.isEmpty()
: "neither username nor password can be blank";
this.host = host;
this.port = port;
this.username = username;
this.password = password;
}
/**
* Creates a NodeProbe using the specified JMX host and port.
*
* @param host hostname or IP address of the JMX agent
* @param port TCP port of the remote JMX agent
*/
public RemoteJmxMBeanAccessor(String host, int port)
{
this.host = host;
this.port = port;
}
/**
* Creates a NodeProbe using the specified JMX host and default port.
*
* @param host hostname or IP address of the JMX agent
*/
public RemoteJmxMBeanAccessor(String host)
{
this(host, defaultPort);
}
/**
* Create a connection to the JMX agent and set up the M[X]Bean proxies.
*/
protected void connect()
{
if (connected)
return;
synchronized (this)
{
if (connected)
return;
try
{
String host = this.host;
if (host.contains(":"))
{
// Use square brackets to surround IPv6 addresses to fix CASSANDRA-7669 and CASSANDRA-17581
host = '[' + host + ']';
}
JMXServiceURL jmxUrl = new JMXServiceURL(String.format(fmtUrl, host, port));
Map<String, Object> env = new HashMap<>();
if (username != null)
{
String[] creds = { username, password };
env.put(JMXConnector.CREDENTIALS, creds);
}
env.put("com.sun.jndi.rmi.factory.socket", getRMIClientSocketFactory());
jmxc = JMXConnectorFactory.connect(jmxUrl, env);
mbeanServerConn = jmxc.getMBeanServerConnection();
registerMBeanProxy(StorageServiceMBean.class, "org.apache.cassandra.db:type=StorageService");
registerMBeanProxy(SnapshotManagerMBean.class, SnapshotManagerMBean.MBEAN_NAME);
registerMBeanProxy(CMSOperationsMBean.class, CMSOperations.MBEAN_OBJECT_NAME);
registerMBeanProxy(AccordOperationsMBean.class, AccordOperations.MBEAN_OBJECT_NAME);
registerMBeanProxy(MessagingServiceMBean.class, MessagingService.MBEAN_NAME);
registerMBeanProxy(StreamManagerMBean.class, StreamManagerMBean.OBJECT_NAME);
registerMBeanProxy(CompactionManagerMBean.class, CompactionManager.MBEAN_OBJECT_NAME);
registerMBeanProxy(FailureDetectorMBean.class, FailureDetector.MBEAN_NAME);
registerMBeanProxy(CacheServiceMBean.class, CacheService.MBEAN_NAME);
registerMBeanProxy(StorageProxyMBean.class, StorageProxy.MBEAN_NAME);
registerMBeanProxy(HintsServiceMBean.class, HintsService.MBEAN_NAME);
registerMBeanProxy(GCInspectorMXBean.class, GCInspector.MBEAN_NAME);
registerMBeanProxy(GossiperMBean.class, Gossiper.MBEAN_NAME);
registerMBeanProxy(BatchlogManagerMBean.class, BatchlogManager.MBEAN_NAME);
registerMBeanProxy(ActiveRepairServiceMBean.class, ActiveRepairServiceMBean.MBEAN_NAME);
registerMBeanProxy(AuditLogManagerMBean.class, AuditLogManager.MBEAN_NAME);
registerMBeanProxy(PasswordAuthenticator.CredentialsCacheMBean.class,
AuthCache.MBEAN_NAME_BASE + PasswordAuthenticator.CredentialsCacheMBean.CACHE_NAME);
registerMBeanProxy(AuthorizationProxy.JmxPermissionsCacheMBean.class,
AuthCache.MBEAN_NAME_BASE + AuthorizationProxy.JmxPermissionsCacheMBean.CACHE_NAME);
registerMBeanProxy(NetworkPermissionsCacheMBean.class,
AuthCache.MBEAN_NAME_BASE + NetworkPermissionsCache.CACHE_NAME);
registerMBeanProxy(PermissionsCacheMBean.class,
AuthCache.MBEAN_NAME_BASE + PermissionsCache.CACHE_NAME);
registerMBeanProxy(RolesCacheMBean.class,
AuthCache.MBEAN_NAME_BASE + RolesCache.CACHE_NAME);
registerMBeanProxy(CIDRPermissionsManagerMBean.class, CIDRPermissionsManager.MBEAN_NAME);
registerMBeanProxy(CIDRGroupsMappingManagerMBean.class, CIDRGroupsMappingManager.MBEAN_NAME);
registerMBeanProxy(CIDRFilteringMetricsTableMBean.class, CIDRFilteringMetricsTable.MBEAN_NAME);
registerMBeanProxy(AutoRepairServiceMBean.class, AutoRepairService.MBEAN_NAME);
registerMBeanProxy(GuardrailsMBean.class, Guardrails.MBEAN_NAME);
registerPlatformMBeanProxy(MemoryMXBean.class, ManagementFactory.MEMORY_MXBEAN_NAME);
registerPlatformMBeanProxy(RuntimeMXBean.class, ManagementFactory.RUNTIME_MXBEAN_NAME);
registerMBeanProxy(EndpointSnitchInfoMBean.class, "org.apache.cassandra.db:type=EndpointSnitchInfo");
registerMBeanProxy(DynamicEndpointSnitchMBean.class, "org.apache.cassandra.db:type=DynamicEndpointSnitch");
registerMBeanProxy(LocationInfoMBean.class, "org.apache.cassandra.db:type=LocationInfo");
registerMBeanProxy(AsyncProfilerMBean.class, AsyncProfilerMBean.MBEAN_NAME);
}
catch (MalformedObjectNameException e)
{
close();
throw new RuntimeException("Invalid ObjectName? Please report this as a bug.", e);
}
catch (IOException | SecurityException e)
{
close();
Throwable rootCause = Throwables.getRootCause(e);
throw new NodetoolConnectionException(String.format("Failed to connect to '%s:%s' - %s: '%s'.",
host, port,
rootCause.getClass().getSimpleName(),
rootCause.getMessage()),
e);
}
connected = true;
}
}
protected <T> void registerMBean(Class<T> clazz, T mbean)
{
clazzMBanRegistry.put(clazz, mbean);
}
@Override
public <T> T findMBean(Class<T> clazz)
{
connect();
return clazzMBanRegistry.get(clazz) == null ? null : clazz.cast(clazzMBanRegistry.get(clazz));
}
@SuppressWarnings("unchecked")
public <T> T findMBeanMetric(Class<T> clazz, Props props)
{
return withExceptionHandling(() -> {
connect();
ObjectName objectName = new ObjectName("org.apache.cassandra.metrics", new Hashtable<>(props.toMap()));
T result = (T) namedMBeanRegistry.computeIfAbsent(objectName.getCanonicalName(),
ignore -> JMX.newMBeanProxy(mbeanServerConn, objectName, clazz));
return clazz.cast(result);
});
}
@Override
public boolean isMBeanMetricRegistered(Props props)
{
return withExceptionHandling(() -> {
connect();
ObjectName objectName = new ObjectName("org.apache.cassandra.metrics", new Hashtable<>(props.toMap()));
return mbeanServerConn.isRegistered(objectName);
});
}
@Override
public CassandraMetricsRegistry.JmxCounterMBean findMBeanCounter(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxCounterMBean.class, props);
}
@Override
public CassandraMetricsRegistry.JmxGaugeMBean findMBeanGauge(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxGaugeMBean.class, props);
}
@Override
public CassandraMetricsRegistry.JmxMeterMBean findMBeanMeter(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxMeterMBean.class, props);
}
@Override
public CassandraMetricsRegistry.JmxTimerMBean findMBeanTimer(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxTimerMBean.class, props);
}
@Override
public CassandraMetricsRegistry.JmxHistogramMBean findMBeanHistogram(Props props)
{
return findMBeanMetric(CassandraMetricsRegistry.JmxHistogramMBean.class, props);
}
@Override
public ColumnFamilyStoreMBean findColumnFamily(String type, String keyspace, String columnFamily)
{
return withExceptionHandling(() -> {
connect();
Set<ObjectName> beans = mbeanServerConn.queryNames(new ObjectName("org.apache.cassandra.db:type=*" + type +
",keyspace=" + keyspace +
",columnfamily=" + columnFamily), null);
if (beans.isEmpty())
throw new MalformedObjectNameException("couldn't find that bean");
assert beans.size() == 1;
return JMX.newMBeanProxy(mbeanServerConn, beans.iterator().next(), ColumnFamilyStoreMBean.class);
});
}
@Override
public CompressionDictionaryManagerMBean findCompressionDictionary(String keyspace, String table)
{
List<Map.Entry<String, ColumnFamilyStoreMBean>> keyspaces = findColumnFamilies("ColumnFamilies");
Optional<ColumnFamilyStoreMBean> cfsMBean = keyspaces.stream()
.filter(e -> e.getKey().equals(keyspace))
.map(Map.Entry::getValue)
.filter(mbean -> mbean.getTableName().equals(table))
.findAny();
if (keyspaces.isEmpty() || cfsMBean.isEmpty())
throw new IllegalArgumentException(String.format("Table %s.%s does not exist", keyspace, table));
return withExceptionHandling(() -> {
connect();
String mbeanName = CompressionDictionaryManagerMBean.MBEAN_NAME + ",keyspace=" + keyspace + ",table=" + table;
if (!mbeanServerConn.isRegistered(new ObjectName(mbeanName)))
throw new IllegalStateException("The compression on table " + keyspace + '.' + table + " is not enabled or SSTable compressor is not a dictionary compressor.");
return JMX.newMBeanProxy(mbeanServerConn, new ObjectName(mbeanName), CompressionDictionaryManagerMBean.class);
});
}
@Override
public List<ThreadPoolInfo> threadPoolInfos()
{
return withExceptionHandling(() -> {
connect();
Set<ObjectName> threadPoolObjectNames = mbeanServerConn.queryNames(new ObjectName("org.apache.cassandra.metrics:type=ThreadPools,*"), null);
return threadPoolObjectNames.stream()
.map(oName -> new ThreadPoolInfo(oName.getKeyProperty("path"), oName.getKeyProperty("scope")))
.collect(Collectors.toList());
});
}
@Override
public List<Map.Entry<String, ColumnFamilyStoreMBean>> findColumnFamilies(String type)
{
return withExceptionHandling(() -> {
assert type.equals("IndexColumnFamilies") || type.equals("ColumnFamilies");
connect();
ObjectName query = new ObjectName("org.apache.cassandra.db:type=" + type + ",*");
Set<ObjectName> cfObjects = mbeanServerConn.queryNames(query, null);
List<Map.Entry<String, ColumnFamilyStoreMBean>> mbeans = new ArrayList<>(cfObjects.size());
for (ObjectName objectName : cfObjects)
{
ColumnFamilyStoreMBean cfsProxy = JMX.newMBeanProxy(mbeanServerConn, objectName, ColumnFamilyStoreMBean.class);
mbeans.add(new AbstractMap.SimpleImmutableEntry<>(objectName.getKeyProperty("keyspace"), cfsProxy));
}
return mbeans;
});
}
public JMXConnector getJmxConnector()
{
connect();
return jmxc;
}
public MBeanServerConnection getMBeanServerConnection()
{
connect();
return mbeanServerConn;
}
@Override
public void close()
{
if (jmxc == null)
return;
try
{
jmxc.close();
}
catch (ConnectException e)
{
// result of stopdaemon command, if close() call fails, the daemon is shutdown
logger.error("Cassandra has shutdown.");
}
catch (IOException e)
{
logger.error("Failed to close connection to '{}:{}'.", host, port, e);
}
finally
{
jmxc = null;
mbeanServerConn = null;
connected = false;
clazzMBanRegistry.clear();
namedMBeanRegistry.clear();
}
}
private <T> void registerMBeanProxy(Class<T> clazz, String objectName) throws MalformedObjectNameException
{
registerMBean(clazz, JMX.newMBeanProxy(mbeanServerConn, new ObjectName(objectName), clazz));
}
private <T> void registerPlatformMBeanProxy(Class<T> clazz, String objectName) throws IOException
{
registerMBean(clazz, ManagementFactory.newPlatformMXBeanProxy(mbeanServerConn, objectName, clazz));
}
private RMIClientSocketFactory getRMIClientSocketFactory()
{
if (SSL_ENABLE.getBoolean())
return new SslRMIClientSocketFactory();
else
return RMISocketFactory.getDefaultSocketFactory();
}
private static <T> T withExceptionHandling(MBeanSupplier<T> op)
{
try
{
return op.get();
}
catch (MalformedObjectNameException e)
{
throw new RuntimeException("Invalid ObjectName? Requested MBean may not exist. " +
"Please check the parameters e.g. keyspace name, table name and try again.", e);
}
catch (IOException e)
{
throw new RuntimeException("Could not connect to MBean server. Please check that the JMX port is correct and open.", e);
}
}
@FunctionalInterface
private interface MBeanSupplier<T>
{
T get() throws MalformedObjectNameException, IOException;
}
}

View File

@ -25,6 +25,7 @@ import java.util.List;
import javax.management.ListenerNotFoundException;
import javax.management.remote.JMXConnector;
import org.apache.cassandra.management.MBeanAccessor;
import org.apache.cassandra.service.ActiveRepairService.ParentRepairStatus;
import org.apache.cassandra.service.StorageServiceMBean;
import org.apache.cassandra.utils.Closeable;
@ -34,9 +35,9 @@ import org.apache.cassandra.utils.progress.ProgressEventType;
import org.apache.cassandra.utils.progress.jmx.JMXNotificationProgressListener;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.cassandra.config.CassandraRelevantProperties.NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS;
import static org.apache.cassandra.service.ActiveRepairService.ParentRepairStatus.FAILED;
import static org.apache.cassandra.service.ActiveRepairService.ParentRepairStatus.valueOf;
import static org.apache.cassandra.tools.NodeProbe.JMX_NOTIFICATION_POLL_INTERVAL_SECONDS;
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition;
@ -46,6 +47,8 @@ import static org.apache.cassandra.utils.progress.ProgressEventType.PROGRESS;
public class RepairRunner extends JMXNotificationProgressListener implements Closeable
{
private final long JMX_NOTIFICATION_POLL_INTERVAL_SECONDS = NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS.getLong();
public static abstract class RepairCmd
{
private final String keyspace;
@ -60,6 +63,7 @@ public class RepairRunner extends JMXNotificationProgressListener implements Clo
private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
private final PrintStream out;
/** The connector to monitor the JMX connection state, so it can be {@code null} for server-side tools. */
private final JMXConnector jmxc;
private final StorageServiceMBean ssProxy;
private final Condition condition = newOneTimeCondition();
@ -68,10 +72,10 @@ public class RepairRunner extends JMXNotificationProgressListener implements Clo
private Integer cmd;
private volatile Exception error;
public RepairRunner(PrintStream out, JMXConnector jmxc, StorageServiceMBean ssProxy, RepairCmd repairCmd)
public RepairRunner(PrintStream out, MBeanAccessor accessor, StorageServiceMBean ssProxy, RepairCmd repairCmd)
{
this.out = out;
this.jmxc = jmxc;
this.jmxc = accessor instanceof RemoteJmxMBeanAccessor ? ((RemoteJmxMBeanAccessor) accessor).getJmxConnector() : null;
this.ssProxy = ssProxy;
this.repairCmd = repairCmd;
}

View File

@ -73,7 +73,7 @@ public abstract class AbstractCommand implements Runnable
* @return {@code true} if the command is required to connect to the node, {@code false} otherwise.
* @throws ExecutionException if an error occurs during preparation and execution must be aborted.
*/
protected boolean shouldConnect() throws ExecutionException
public boolean shouldConnect() throws ExecutionException
{
return true;
}

View File

@ -303,6 +303,8 @@ public class CMSAdmin extends AbstractCommand
@Command(name = "dump", description = "Dumps cluster metadata into a file")
public static class DumpClusterMetadata extends AbstractCommand
{
// TODO CASSANDRA-XXXXX @ArgGroup cannot be populated by the remote (CQL/MBean) execution path,
// this should be avoided to keep the command hierarchy flat for all interfaces
@ArgGroup(exclusive = false, multiplicity = "0..1")
DumpOptions dumpOptions;

View File

@ -136,9 +136,10 @@ public class CompressionDictionaryCommandGroup
else if (TrainingStatus.FAILED == status)
{
err.printf("%nTraining failed for %s.%s%n", keyspace, table);
String failureMessage = null;
try
{
String failureMessage = trainingState.getFailureMessage();
failureMessage = trainingState.getFailureMessage();
if (failureMessage != null && !failureMessage.isEmpty())
{
err.printf("Reason: %s%n", failureMessage);
@ -148,19 +149,23 @@ public class CompressionDictionaryCommandGroup
{
// If we can't get the failure message, just continue without it
}
System.exit(1);
throw new RuntimeException(String.format("Training failed for %s.%s. Reason: %s",
keyspace, table, failureMessage == null ?
"undefined" : failureMessage));
}
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
}
err.printf("%nTraining did not complete within expected timeframe (10 minutes).%n");
System.exit(1);
throw new RuntimeException("Training did not complete within expected timeframe (10 minutes).");
}
catch (IllegalArgumentException | IllegalStateException e)
{
throw new IllegalArgumentException("Failed to trigger training: " + e.getMessage(), e);
}
catch (Exception e)
{
err.printf("Failed to trigger training: %s%n", e.getMessage());
System.exit(1);
throw new RuntimeException("Failed to trigger training: " + e.getMessage(), e);
}
}
@ -187,7 +192,7 @@ public class CompressionDictionaryCommandGroup
catch (Throwable t)
{
err.println("Invalid value for " + MAX_DICT_SIZE_PARAM_NAME + ": " + t.getMessage());
System.exit(1);
throw t;
}
}
@ -200,7 +205,7 @@ public class CompressionDictionaryCommandGroup
catch (Throwable t)
{
err.println("Invalid value for " + MAX_TOTAL_SAMPLE_SIZE_PARAM_NAME + ": " + t.getMessage());
System.exit(1);
throw t;
}
}
}
@ -283,7 +288,7 @@ public class CompressionDictionaryCommandGroup
if (dictId <= 0 && dictId != -1)
{
probe.output().err.printf("Dictionary id has to be strictly positive number.%n");
System.exit(1);
throw new IllegalArgumentException("Dictionary id has to be strictly positive number.");
}
try
@ -304,7 +309,9 @@ public class CompressionDictionaryCommandGroup
probe.output().err.printf("Dictionary%s does not exist for %s.%s.%n",
dictId == -1 ? "" : " with id " + dictId,
keyspace, table);
System.exit(1);
throw new IllegalArgumentException(String.format("Dictionary%s does not exist for %s.%s.",
dictId == -1 ? "" : " with id " + dictId,
keyspace, table));
}
CompressionDictionaryDataObject dataObject = CompressionDictionaryDetailsTabularData.fromCompositeData(compressionDictionary);
@ -314,7 +321,7 @@ public class CompressionDictionaryCommandGroup
catch (Throwable e)
{
probe.output().err.printf("Failed to export dictionary: %s%n", e.getMessage());
System.exit(1);
throw new RuntimeException(e);
}
}
}
@ -339,19 +346,22 @@ public class CompressionDictionaryCommandGroup
CompositeData compositeData = CompressionDictionaryDetailsTabularData.fromCompressionDictionaryDataObject(dictionaryDataObject);
probe.importCompressionDictionary(compositeData);
}
catch (IllegalStateException | IllegalArgumentException ex)
{
// These exceptions are threated as the command input was invalid, so we don't need to wrap them.
throw ex;
}
catch (ValueInstantiationException ex)
{
// we catch this when validation of data object fails - that will happen when
// JSON is invalid, and we attempt to deserialize it to data object by Jackson
// We can fail fast on the client, so we will never reach Cassandra node with a payload
// which would fail there too, so we do not contact a node unnecessarily.
probe.output().err.printf("Unable to import dictionary JSON: %s%n", ex.getCause().getMessage());
System.exit(1);
throw new RuntimeException(String.format("Unable to import dictionary JSON: %s%n", ex.getCause().getMessage()), ex);
}
catch (Throwable t)
{
probe.output().err.printf("Unable to import dictionary JSON: %s%n", t.getMessage());
System.exit(1);
throw new RuntimeException(String.format("Unable to import dictionary JSON: %s%n", t.getMessage()), t);
}
}
@ -360,17 +370,17 @@ public class CompressionDictionaryCommandGroup
if (!dictionaryFile.exists())
{
probe.output().err.printf("Path %s does not exist.%n", dictionaryPath);
System.exit(1);
throw new IllegalArgumentException("Path " + dictionaryPath + " does not exist.");
}
if (!dictionaryFile.isFile())
{
probe.output().err.printf("Path %s is not a file.%n", dictionaryPath);
System.exit(1);
throw new IllegalArgumentException("Path " + dictionaryPath + " is not a file.");
}
if (!dictionaryFile.isReadable())
{
probe.output().err.printf("Path %s is not readable.%n", dictionaryPath);
System.exit(1);
throw new IllegalArgumentException("Path " + dictionaryPath + " is not readable.");
}
}
}
@ -431,8 +441,7 @@ public class CompressionDictionaryCommandGroup
}
catch (Exception e)
{
probe.output().err.printf("Failed to list dictionaries: %s%n", e.getMessage());
System.exit(1);
throw new RuntimeException("Failed to list dictionaries: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,141 @@
/*
* 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.tools.nodetool;
import java.io.IOException;
import com.google.common.base.Throwables;
import org.apache.cassandra.config.CassandraRelevantEnv;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.transport.SimpleClient;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;
/**
* Command options for NodeTool commands that are executed via CQL.
*/
@Command(name = "cqlconnect", description = "Connect to a Cassandra node via CQL")
public class CqlConnect extends AbstractCommand implements AutoCloseable
{
private static final int DEFAULT_CQL_PORT = 11211;
/** The command specification, used to access command-specific properties. */
@Spec
protected CommandSpec spec; // injected by picocli
@Option(names = { "-h", "--host" }, description = "Node hostname or ip address", arity = "0..1")
private String host = "127.0.0.1";
@Option(names = { "-p", "--port" }, description = "Remote CQL native transport port number", arity = "0..1")
private int port = DEFAULT_CQL_PORT;
@Option(names = { "--diagnostic" }, description = "Enable diagnostic output for troubleshooting connection issues")
private boolean diagnostic = false;
private volatile SimpleClient client;
/**
* Initialize the CQL connection to the Cassandra node using the provided options.
*/
public void run()
{
if (client != null)
return;
try
{
if (diagnostic)
output.printInfo("Connecting to %s:%s via CQL...%n", host, port);
SimpleClient.Builder builder = SimpleClient.builder(host, port)
.protocolVersion(ProtocolVersion.V5)
.requestTimeoutSeconds(requestTimeoutSeconds());
client = builder.build();
client.connect(false);
}
catch (IOException e)
{
Throwable rootCause = Throwables.getRootCause(e);
throw new NodetoolConnectionException(String.format("Failed to connect to '%s:%s' via CQL - %s: '%s'.",
host, port,
rootCause.getClass().getSimpleName(),
rootCause.getMessage()),
e);
}
}
public SimpleClient client()
{
return client;
}
/**
* Long-running commands are executed asynchronously by default, but some commands (e.g. info)
* are still synchronous and may need longer than {@link SimpleClient#TIMEOUT_SECONDS} to
* respond; {@code 0} waits indefinitely.
*/
private static long requestTimeoutSeconds()
{
String env = CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getString();
if (env == null)
return CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getLong();
try
{
return Long.parseLong(env);
}
catch (NumberFormatException e)
{
throw new IllegalArgumentException(String.format("Invalid value for environment variable '%s': " +
"expected a number of seconds but was '%s'",
CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getKey(),
env));
}
}
@Override
protected void execute(NodeProbe probe)
{
assert probe == null;
run();
}
@Override
public void close() throws Exception
{
if (client != null)
{
try
{
client.close();
}
finally
{
client = null;
}
}
}
}

View File

@ -61,6 +61,7 @@ public class ForceCompact extends AbstractCommand
try
{
probe.forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace);
probe.output().printInfo("Force compaction performed for keyspace '%s', table '%s'", keyspaceName, tableName);
}
catch (Exception e)
{

View File

@ -51,7 +51,7 @@ public class History extends AbstractCommand implements LocalCommand
}
@Override
protected boolean shouldConnect() throws CommandLine.ExecutionException
public boolean shouldConnect() throws CommandLine.ExecutionException
{
return false;
}

View File

@ -26,6 +26,8 @@ import java.util.Map.Entry;
import javax.management.InstanceNotFoundException;
import com.google.common.base.Throwables;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.service.CacheServiceMBean;
@ -73,7 +75,7 @@ public class Info extends AbstractCommand
catch (RuntimeException e)
{
// offheap-metrics introduced in 2.1.3 - older versions do not have the appropriate mbeans
if (!(e.getCause() instanceof InstanceNotFoundException))
if (!(Throwables.getRootCause(e) instanceof InstanceNotFoundException))
throw e;
}
@ -135,7 +137,7 @@ public class Info extends AbstractCommand
}
catch (RuntimeException e)
{
if (!(e.getCause() instanceof InstanceNotFoundException))
if (!(Throwables.getRootCause(e) instanceof InstanceNotFoundException))
throw e;
// Chunk cache is not on.
@ -151,7 +153,7 @@ public class Info extends AbstractCommand
}
catch (RuntimeException e)
{
if (!(e.getCause() instanceof InstanceNotFoundException))
if (!(Throwables.getRootCause(e) instanceof InstanceNotFoundException))
throw e;
// network cache is not on.

View File

@ -22,7 +22,6 @@ import java.io.Console;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Scanner;
import javax.inject.Inject;
@ -32,21 +31,15 @@ import com.google.common.base.Throwables;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.tools.INodeProbeFactory;
import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.ExecutionException;
import picocli.CommandLine.IExecutionStrategy;
import picocli.CommandLine.InitializationException;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.RunLast;
import picocli.CommandLine.Spec;
import static java.lang.Integer.parseInt;
import static org.apache.cassandra.tools.NodeProbe.defaultPort;
import static org.apache.cassandra.tools.RemoteJmxMBeanAccessor.defaultPort;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@ -57,8 +50,6 @@ import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@Command(name = "connect", description = "Connect to a Cassandra node via JMX")
public class JmxConnect extends AbstractCommand implements AutoCloseable
{
public static final String MIXIN_KEY = "jmx";
/** The command specification, used to access command-specific properties. */
@Spec
protected CommandSpec spec; // injected by picocli
@ -81,30 +72,6 @@ public class JmxConnect extends AbstractCommand implements AutoCloseable
@Inject
private INodeProbeFactory nodeProbeFactory;
/**
* This method is called by picocli and used depending on the execution strategy.
* @param parseResult The parsed command line.
* @return The exit code.
*/
public static int executionStrategy(ParseResult parseResult)
{
CommandSpec jmx = parseResult.commandSpec().mixins().get(MIXIN_KEY);
if (jmx == null)
throw new InitializationException("No JmxConnect command found in the top-level hierarchy");
try (JmxConnectionCommandInvoker invoker = new JmxConnectionCommandInvoker((JmxConnect) jmx.userObject()))
{
return invoker.execute(parseResult);
}
catch (JmxConnectionCommandInvoker.CloseException e)
{
jmx.commandLine()
.getErr()
.println("Failed to connect to JMX: " + e.getMessage());
return jmx.commandLine().getExitCodeExceptionMapper().getExitCode(e);
}
}
/**
* Initialize the JMX connection to the Cassandra node using the provided options.
*/
@ -129,9 +96,11 @@ public class JmxConnect extends AbstractCommand implements AutoCloseable
catch (IOException | SecurityException e)
{
Throwable rootCause = Throwables.getRootCause(e);
output.printError("nodetool: Failed to connect to '%s:%s' - %s: '%s'.%n", host, port,
rootCause.getClass().getSimpleName(), rootCause.getMessage());
throw new InitializationException("Failed to connect to JMX", e);
throw new NodetoolConnectionException(String.format("Failed to connect to '%s:%s' - %s: '%s'.",
host, port,
rootCause.getClass().getSimpleName(),
rootCause.getMessage()),
e);
}
}
@ -183,61 +152,13 @@ public class JmxConnect extends AbstractCommand implements AutoCloseable
return password;
}
private static class JmxConnectionCommandInvoker implements IExecutionStrategy, AutoCloseable
public String getHost()
{
private final JmxConnect connect;
return host;
}
public JmxConnectionCommandInvoker(JmxConnect connect)
{
this.connect = connect;
}
@Override
public int execute(ParseResult parseResult) throws ExecutionException, ParameterException
{
CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
if (lastParent.userObject() instanceof AbstractCommand)
{
AbstractCommand command = (AbstractCommand) lastParent.userObject();
if (command.shouldConnect())
connect.run();
command.probe(connect.probe());
}
return new RunLast().execute(parseResult);
}
@Override
public void close() throws CloseException
{
try
{
if (connect.probe() != null)
((AutoCloseable) connect.probe()).close();
}
catch (Exception e)
{
throw new CloseException("Failed to close JMX connection", e);
}
}
private static CommandLine.Model.CommandSpec lastExecutableSubcommandWithSameParent(List<CommandLine> parsedCommands)
{
int start = parsedCommands.size() - 1;
for (int i = parsedCommands.size() - 2; i >= 0; i--)
{
if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent())
break;
start = i;
}
return parsedCommands.get(start).getCommandSpec();
}
private static class CloseException extends RuntimeException
{
public CloseException(String message, Throwable cause)
{
super(message, cause);
}
}
public String getPort()
{
return port;
}
}

View File

@ -19,6 +19,7 @@ package org.apache.cassandra.tools.nodetool;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLDecoder;
@ -47,6 +48,7 @@ import org.gridkit.jvmtool.cli.CommandLauncher;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.Output;
import org.apache.cassandra.tools.RemoteJmxMBeanAccessor;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@ -64,7 +66,7 @@ public class Sjk extends AbstractCommand
private final Wrapper wrapper = new Wrapper();
@Override
protected boolean shouldConnect() throws ExecutionException
public boolean shouldConnect() throws ExecutionException
{
// We want to parse the given arguments in advance to determine if the SJK command requires an MBeanServerConnection or not.
wrapper.prepare(args.isEmpty() ? new String[]{ "--help" } : args.toArray(new String[0]), output.out, output.err);
@ -243,7 +245,9 @@ public class Sjk extends AbstractCommand
{
public MBeanServerConnection getMServer()
{
return probe.getMbeanServerConn();
return probe.getMBeanAccessor() instanceof RemoteJmxMBeanAccessor ?
((RemoteJmxMBeanAccessor) probe.getMBeanAccessor()).getMBeanServerConnection() :
ManagementFactory.getPlatformMBeanServer();
}
});
}

View File

@ -18,8 +18,11 @@
package org.apache.cassandra.tools.nodetool;
import com.google.common.base.Throwables;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
import org.apache.cassandra.utils.JVMStabilityInspector;
import picocli.CommandLine.Command;
@ -37,6 +40,12 @@ public class StopDaemon extends AbstractCommand
} catch (Exception e)
{
JVMStabilityInspector.inspectThrowable(e);
// The connection dropping while the daemon stops is expected and ignored.
if (Throwables.getCausalChain(e).stream().anyMatch(NodetoolConnectionException.class::isInstance))
{
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
}
// ignored
}
}

View File

@ -33,6 +33,7 @@ import java.util.TimeZone;
import javax.management.InstanceNotFoundException;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import org.apache.commons.lang3.time.DurationFormatUtils;
@ -341,7 +342,7 @@ public class TableStatsHolder implements StatsHolder
catch (RuntimeException e)
{
// offheap-metrics introduced in 2.1.3 - older versions do not have the appropriate mbeans
if (!(e.getCause() instanceof InstanceNotFoundException))
if (!(Throwables.getRootCause(e) instanceof InstanceNotFoundException))
throw e;
}

View File

@ -0,0 +1,42 @@
/*
* 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.tools.nodetool.strategy;
import picocli.CommandLine;
public interface CommandExecutionStrategy extends CommandLine.IExecutionStrategy, AutoCloseable
{
@Override
default void close() throws ExecutionStrategyCloseException {}
class ExecutionStrategyCloseException extends RuntimeException
{
public ExecutionStrategyCloseException(String message, Throwable cause)
{
super(message, cause);
}
}
enum Type
{
CQL,
STATIC_MBEAN,
COMMAND_MBEAN,
}
}

View File

@ -0,0 +1,205 @@
/*
* 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.tools.nodetool.strategy;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.management.MBeanException;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeMBeanException;
import com.google.common.base.Strings;
import org.apache.cassandra.config.CassandraRelevantEnv;
import org.apache.cassandra.cql3.statements.ExecuteCommandStatement;
import org.apache.cassandra.management.CommandExecutionArgsSerde;
import org.apache.cassandra.management.CommandMBeanAdapter;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.picocli.PicocliCommandArgsConverter;
import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.RemoteJmxMBeanAccessor;
import org.apache.cassandra.tools.nodetool.AbstractCommand;
import org.apache.cassandra.tools.nodetool.JmxConnect;
import org.apache.cassandra.tools.nodetool.StopDaemon;
import picocli.CommandLine;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID;
import static org.apache.cassandra.management.ManagementUtils.fullCommandName;
import static org.apache.cassandra.utils.JsonUtils.fromJsonMap;
public class CommandMBeanExecutionStrategy implements CommandExecutionStrategy
{
private static final String MBEAN_DOMAIN = "org.apache.cassandra.management";
private static final String MBEAN_TYPE_COMMAND = "Command";
private final JmxConnect connect;
public CommandMBeanExecutionStrategy(JmxConnect connect)
{
this.connect = connect;
}
@Override
public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException, CommandLine.ParameterException
{
CommandLine.Model.CommandSpec lastParent = StaticMBeanExecutionStrategy.lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
NodeProbe probe = null;
Object userObject = lastParent.userObject();
if (userObject instanceof AbstractCommand)
{
AbstractCommand command = (AbstractCommand) userObject;
if (command.shouldConnect())
connect.run();
probe = connect.probe();
}
// Local command execution with no JMX connection.
if (probe == null || parseResult.isUsageHelpRequested() || parseResult.isVersionHelpRequested())
return new CommandLine.RunLast().execute(parseResult);
String commandName = extractCommandName(parseResult);
if (commandName == null || commandName.isEmpty())
return new CommandLine.RunLast().execute(parseResult);
// Command is already populated with args from CommandLine.parseArgs(), convert to CommandExecutionArgs
CommandExecutionArgs args = PicocliCommandArgsConverter.fromCommand(userObject);
String jsonParams = CommandExecutionArgsSerde.toJson(args);
try
{
MBeanServerConnection mbs = ((RemoteJmxMBeanAccessor) probe.getMBeanAccessor()).getMBeanServerConnection();
if (!mbs.isRegistered(constructCommandMBeanName(commandName)))
return new CommandLine.RunLast().execute(parseResult);
String rawResult = executeViaRemoteMBean(mbs, commandName, jsonParams);
Map<String, String> commandResult = fromJsonMap(rawResult);
String executionId = commandResult.get(ExecuteCommandStatement.COMMAND_RESULT_SCHEMA_EXECUTION_ID);
String output = commandResult.get(ExecuteCommandStatement.COMMAND_RESULT_SCHEMA_OUTPUT);
if (Strings.isNullOrEmpty(executionId) || output == null)
throw new RuntimeException("Invalid command result schema received from CommandMBeanAdapter: " + rawResult);
probe.output().printInfo(output);
// The output needs to be consistent for the tests we already have and for thouse which heavily rely on
// asserting command output, so we use this property to control whether the execution id is printed or not.
if (CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID.getBoolean() ||
CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID.getBoolean())
probe.output().printInfo("Command execution id: " + executionId);
return 0;
}
catch (NodetoolConnectionException e)
{
throw e;
}
catch (RuntimeMBeanException e)
{
Throwable cause = e.getCause();
if (cause instanceof IllegalArgumentException || cause instanceof IllegalStateException)
{
throw new CommandLine.ParameterException(parseResult.commandSpec().commandLine(),
"Invalid command parameters: " + cause.getMessage(), cause);
}
else
{
// Include security exception, instance not found, invocation target exceptions.
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
"Failed to execute command via MBean: " + cause.getMessage(), cause);
}
}
catch (IOException e)
{
if (userObject instanceof StopDaemon)
{
probe.output().printInfo("Cassandra has shutdown.");
return 0;
}
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
String.format("JMX connection to the node was lost while executing command '%s'. " +
"The command may still be running on the server.",
commandName),
e);
}
catch (Exception e)
{
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
"Unexpected error during command execution: " + e.getMessage(), e);
}
}
@Override
public void close() throws ExecutionStrategyCloseException
{
if (connect.probe() == null)
return;
try
{
connect.probe().close();
}
catch (Exception e)
{
throw new ExecutionStrategyCloseException("Failed to close JMX connection", e);
}
}
/** Handles nested commands (e.g. compressiondictionary.train). */
public static String extractCommandName(CommandLine.ParseResult parseResult)
{
List<CommandLine> commandLineList = parseResult.asCommandLineList();
if (commandLineList.size() <= 1) // Only root "nodetool"
return null;
// Skip the root command ("nodetool") and concatenate the rest to form the full command name.
return fullCommandName(commandLineList.subList(1, commandLineList.size()), CommandLine::getCommandName);
}
private String executeViaRemoteMBean(MBeanServerConnection mbs, String commandName, String jsonParams) throws Exception
{
ObjectName mbeanName = constructCommandMBeanName(commandName);
try
{
Object result = mbs.invoke(mbeanName, CommandMBeanAdapter.INVOKE_METHOD,
new Object[]{ jsonParams },
new String[]{ String.class.getName() });
return result == null ? "" : result.toString();
}
catch (MBeanException | ReflectionException e)
{
throw new RuntimeMBeanException(new RuntimeException(e), "Failed to invoke CommandMBeanAdapter: " + commandName);
}
}
private ObjectName constructCommandMBeanName(String commandName) throws Exception
{
String escapedName = ObjectName.quote(commandName);
String objectNameStr = String.format("%s:type=%s,name=%s", MBEAN_DOMAIN, MBEAN_TYPE_COMMAND, escapedName);
return new ObjectName(objectNameStr);
}
}

View File

@ -0,0 +1,330 @@
/*
* 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.tools.nodetool.strategy;
import java.lang.reflect.Array;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.config.CassandraRelevantEnv;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.CqlBuilder;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.marshal.ByteArrayAccessor;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.UUIDType;
import org.apache.cassandra.exceptions.CommandRequestExecutionException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.management.api.CommandExecutionArgs;
import org.apache.cassandra.management.api.OptionMetadata;
import org.apache.cassandra.management.api.ParameterMetadata;
import org.apache.cassandra.management.picocli.PicocliCommandArgsConverter;
import org.apache.cassandra.tools.nodetool.AbstractCommand;
import org.apache.cassandra.tools.nodetool.CqlConnect;
import org.apache.cassandra.tools.nodetool.StopDaemon;
import org.apache.cassandra.transport.SimpleClient;
import org.apache.cassandra.transport.messages.ResultMessage;
import picocli.CommandLine;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID;
import static org.apache.cassandra.management.ManagementUtils.normalizeOptionName;
import static org.apache.cassandra.management.api.ParameterMetadata.COMMAND_POSITIONAL_PARAM_PREFIX;
import static org.apache.cassandra.tools.nodetool.strategy.CommandMBeanExecutionStrategy.extractCommandName;
public class CqlCommandExecutionStrategy implements CommandExecutionStrategy
{
private final CqlConnect connect;
public CqlCommandExecutionStrategy(CqlConnect connect)
{
this.connect = connect;
}
@Override
public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException, CommandLine.ParameterException
{
CommandLine.Model.CommandSpec lastParent = StaticMBeanExecutionStrategy.lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
Object userObject = lastParent.userObject();
if (userObject instanceof AbstractCommand)
{
AbstractCommand command = (AbstractCommand) userObject;
if (command.shouldConnect())
connect.run();
}
// Local command execution with no CQL connection.
if (connect.client() == null || parseResult.isUsageHelpRequested() || parseResult.isVersionHelpRequested())
return new CommandLine.RunLast().execute(parseResult);
String commandName = extractCommandName(parseResult);
if (commandName == null || commandName.isEmpty())
return new CommandLine.RunLast().execute(parseResult);
// Command is already populated with args from CommandLine.parseArgs(), converting it to CommandExecutionArgs
CommandExecutionArgs args = PicocliCommandArgsConverter.fromCommand(userObject);
try
{
String cqlCommand = buildCqlCommandString(commandName, args);
ResultMessage result = connect.client().execute(cqlCommand, ConsistencyLevel.ONE);
if (result instanceof ResultMessage.Rows)
{
ResultMessage.Rows rows = (ResultMessage.Rows) result;
assert rows.result.size() == 1 : "Command execution result should have exactly 1 row, got " + rows.result.size();
assert rows.result.metadata.getColumnCount() == 2 : "Command execution result schema has been changed. " +
"Expected 2 columns in result, got " + rows.result.metadata.getColumnCount();
if (!rows.result.isEmpty() && rows.result.metadata.names.size() >= 2)
{
List<byte[]> firstRow = rows.result.rows.get(0);
if (firstRow.size() >= 2)
{
UUID executionId = UUIDType.instance.getSerializer().deserialize(firstRow.get(0), ByteArrayAccessor.instance);
String output = UTF8Type.instance.getSerializer().deserialize(firstRow.get(1), ByteArrayAccessor.instance);
// NodeProbe instance is not available here, so print directly to the command output.
parseResult.commandSpec().commandLine().getOut().println(output);
if (CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID.getBoolean()
|| CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID.getBoolean())
parseResult.commandSpec().commandLine().getOut().println("Command execution id: " + executionId.toString());
}
}
}
return 0;
}
catch (SimpleClient.ConnectionClosedException e)
{
if (userObject instanceof StopDaemon)
{
parseResult.commandSpec().commandLine().getOut().println("Cassandra has shutdown.");
return 0;
}
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
String.format("Connection to the node was closed before a response " +
"was received for command '%s'. " +
"The command may still be running on the server.",
commandName),
e);
}
catch (SimpleClient.TimeoutException e)
{
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
String.format("No response received for command '%s' within the client timeout. " +
"The command may still be running on the server. " +
"Increase the timeout via the %s environment variable " +
"or the %s system property (0 waits indefinitely). %s",
commandName,
CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getKey(),
CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getKey(),
e.getMessage()),
e);
}
catch (RuntimeException e)
{
Throwable cause = e.getCause();
if (cause instanceof UnauthorizedException)
{
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
"Unauthorized: " + cause.getMessage());
}
else if (cause instanceof InvalidRequestException)
{
// CQL invalid request (e.g. validating params and/or param values) translates into picocli param exection.
throw new CommandLine.ParameterException(parseResult.commandSpec().commandLine(),
"Invalid request: " + cause.getMessage());
}
else if (cause instanceof CommandRequestExecutionException)
{
CommandRequestExecutionException cree = (CommandRequestExecutionException) cause;
String msg = String.format("Command execution failed (executionId: %s): %s",
cree.getCommandExecutionId(), cree.getMessage());
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(), msg);
}
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
"Unknown command execution exception via CQL: " + e.getMessage(), e);
}
catch (Exception e) {
// Catch-all for checked exceptions thrown during command execution.
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
"Unknown command execution exception via CQL: " + e.getMessage(), e);
}
}
@Override
public void close() throws ExecutionStrategyCloseException
{
if (connect == null)
return;
try
{
connect.close();
}
catch (Exception e)
{
throw new ExecutionStrategyCloseException("Failed to close CQL connection", e);
}
}
/**
* Build a CQL INVOKE COMMAND statement string from command name and CommandExecutionArgs.
* The format is: INVOKE COMMAND commandName WITH "key1" = 'value1' AND "key2" = 'value2';
*/
@VisibleForTesting
static String buildCqlCommandString(String commandName, CommandExecutionArgs args)
{
CqlBuilder builder = new CqlBuilder();
builder.append("INVOKE COMMAND ");
builder.appendQuotingIfNeeded(commandName);
Map<String, Object> paramsMap = new LinkedHashMap<>();
for (Map.Entry<OptionMetadata, Object> entry : args.options().entrySet())
{
OptionMetadata optionMetadata = entry.getKey();
Object value = entry.getValue();
if (value == null)
continue;
String key = normalizeOptionName(optionMetadata.paramLabel());
paramsMap.put(key, value);
}
for (Map.Entry<ParameterMetadata, Object> entry : args.parameters().entrySet())
{
ParameterMetadata paramMetadata = entry.getKey();
Object value = entry.getValue();
if (value == null)
continue;
String key = COMMAND_POSITIONAL_PARAM_PREFIX + paramMetadata.index();
paramsMap.put(key, value);
}
if (!paramsMap.isEmpty())
{
builder.append(" WITH");
boolean first = true;
for (Map.Entry<String, Object> entry : paramsMap.entrySet())
{
if (first)
builder.append(" ");
else
builder.append(" AND ");
// Using ColumnIdentifier.maybeQuote as some of the keys
// may be reserved words in CQL (e.g., "keyspace", "table").
builder.append(ColumnIdentifier.maybeQuote(entry.getKey()));
builder.append(" = ");
appendCqlValue(builder, entry.getValue());
first = false;
}
}
builder.append(";");
return builder.toString();
}
/** Append a value to CqlBuilder with proper type handling and escaping. */
private static void appendCqlValue(CqlBuilder builder, Object value)
{
if (value == null)
{
builder.append("NULL");
return;
}
if (value instanceof String)
builder.appendWithSingleQuotes((String) value);
else if (value instanceof Enum)
builder.appendWithSingleQuotes(((Enum<?>) value).name());
else if (value instanceof Number)
builder.append(value);
else if (value instanceof Boolean)
builder.append(value);
else if (value instanceof List)
{
List<?> list = (List<?>) value;
builder.append("[");
for (int i = 0; i < list.size(); i++)
{
if (i > 0)
builder.append(", ");
appendCqlValue(builder, list.get(i));
}
builder.append("]");
}
else if (value instanceof Map)
{
Map<String, Object> map = (Map<String, Object>) value;
builder.append("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet())
{
if (!first)
builder.append(", ");
builder.appendWithSingleQuotes(entry.getKey());
builder.append(": ");
appendCqlValue(builder, entry.getValue());
first = false;
}
builder.append("}");
}
else if (value instanceof Set)
{
Set<?> set = (Set<?>) value;
builder.append("{");
boolean first = true;
for (Object item : set)
{
if (!first)
builder.append(", ");
appendCqlValue(builder, item);
first = false;
}
builder.append("}");
}
else if (value.getClass().isArray())
{
int length = Array.getLength(value);
builder.append("[");
for (int i = 0; i < length; i++)
{
if (i > 0)
builder.append(", ");
appendCqlValue(builder, Array.get(value, i));
}
builder.append("]");
}
else
builder.appendWithSingleQuotes(value.toString());
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.tools.nodetool.strategy;
/**
* Thrown when nodetool fails to establish its connection to the target node, over either JMX or CQL.
*/
public class NodetoolConnectionException extends RuntimeException
{
public NodetoolConnectionException(String message, Throwable cause)
{
super(message, cause);
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.tools.nodetool.strategy;
import org.apache.cassandra.config.CassandraRelevantEnv;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.tools.nodetool.CqlConnect;
import org.apache.cassandra.tools.nodetool.JmxConnect;
import picocli.CommandLine;
import static org.apache.cassandra.utils.LocalizeString.toUpperCaseLocalized;
public class ProtocolAwareExecutionStrategy implements CommandExecutionStrategy
{
public static int executionStrategy(CommandLine.ParseResult parseResult)
{
try (ProtocolAwareExecutionStrategy strategy = new ProtocolAwareExecutionStrategy())
{
return strategy.execute(parseResult);
}
}
/**
* This method is called by picocli and used depending on the execution strategy.
* @param parseResult The parsed command line.
* @return The exit code.
*/
@Override
public int execute(CommandLine.ParseResult parseResult)
{
CommandLine.Model.CommandSpec connectCmd = parseResult.commandSpec()
.mixins()
.get(getExecutionStrategyTypeFromEnvAndSys().toString());
if (connectCmd == null)
throw new CommandLine.InitializationException("No 'connection' command found in the top-level hierarchy");
try (CommandExecutionStrategy invoker = createInvoker(connectCmd.userObject()))
{
return invoker.execute(parseResult);
}
catch (NodetoolConnectionException e)
{
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(), e.getMessage(), e);
}
catch (ExecutionStrategyCloseException e)
{
CommandLine commandLine = parseResult.commandSpec().commandLine();
commandLine.getErr().println("Failed to close connection: " + e.getMessage());
CommandLine.IExitCodeExceptionMapper mapper = commandLine.getExitCodeExceptionMapper();
return mapper != null ? mapper.getExitCode(e) : commandLine.getCommandSpec().exitCodeOnExecutionException();
}
}
public static Type getExecutionStrategyTypeFromEnvAndSys()
{
Type defaultStrategy = Type.valueOf(toUpperCaseLocalized(CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_PROTOCOL.getDefaultValue()));
Type strategyEnv = CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_PROTOCOL.getEnum(true, Type.class, defaultStrategy.name());
Type strategySys = CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_PROTOCOL.getEnum(true, Type.class);
return strategyEnv != defaultStrategy ? strategyEnv : strategySys;
}
protected CommandExecutionStrategy createInvoker(Object connectCmd)
{
Type strategy = getExecutionStrategyTypeFromEnvAndSys();
switch (strategy)
{
case CQL:
return new CqlCommandExecutionStrategy((CqlConnect) connectCmd);
case COMMAND_MBEAN:
return new CommandMBeanExecutionStrategy((JmxConnect) connectCmd);
case STATIC_MBEAN:
return new StaticMBeanExecutionStrategy((JmxConnect) connectCmd);
default:
throw new IllegalStateException("Unknown execution strategy: " + strategy);
}
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.tools.nodetool.strategy;
import java.util.List;
import org.apache.cassandra.tools.nodetool.AbstractCommand;
import org.apache.cassandra.tools.nodetool.JmxConnect;
import picocli.CommandLine;
public class StaticMBeanExecutionStrategy implements CommandExecutionStrategy
{
private final JmxConnect connect;
public StaticMBeanExecutionStrategy(JmxConnect connect)
{
this.connect = connect;
}
@Override
public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException, CommandLine.ParameterException
{
CommandLine.Model.CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
if (lastParent.userObject() instanceof AbstractCommand)
{
AbstractCommand command = (AbstractCommand) lastParent.userObject();
if (command.shouldConnect())
connect.run();
command.probe(connect.probe());
}
return new CommandLine.RunLast().execute(parseResult);
}
@Override
public void close() throws ExecutionStrategyCloseException
{
if (connect.probe() == null)
return;
try
{
connect.probe().close();
}
catch (Exception e)
{
throw new ExecutionStrategyCloseException("Failed to close JMX connection", e);
}
}
static CommandLine.Model.CommandSpec lastExecutableSubcommandWithSameParent(List<CommandLine> parsedCommands)
{
int start = parsedCommands.size() - 1;
for (int i = parsedCommands.size() - 2; i >= 0; i--)
{
if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent())
break;
start = i;
}
return parsedCommands.get(start).getCommandSpec();
}
}

View File

@ -23,6 +23,7 @@ import io.netty.util.AttributeKey;
public class Connection
{
static final AttributeKey<Connection> attributeKey = AttributeKey.valueOf("CONN");
static final AttributeKey<Boolean> managementKey = AttributeKey.valueOf("IS_MANAGEMENT_CONN");
private final Channel channel;
private final ProtocolVersion version;

View File

@ -33,9 +33,16 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.DebuggableTask;
import org.apache.cassandra.concurrent.LocalAwareExecutorPlus;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.statements.ExecuteCommandStatement;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.cql3.statements.UseStatement;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.OverloadedException;
import org.apache.cassandra.metrics.ClientMetrics;
import org.apache.cassandra.net.FrameEncoder;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.service.reads.thresholds.CoordinatorWarnings;
@ -44,6 +51,7 @@ import org.apache.cassandra.transport.ClientResourceLimits.Overload;
import org.apache.cassandra.transport.Flusher.FlushItem;
import org.apache.cassandra.transport.messages.ErrorMessage;
import org.apache.cassandra.transport.messages.EventMessage;
import org.apache.cassandra.transport.messages.QueryMessage;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.MonotonicClock;
import org.apache.cassandra.utils.NoSpamLogger;
@ -53,6 +61,7 @@ import io.netty.channel.EventLoop;
import io.netty.util.AttributeKey;
import static org.apache.cassandra.concurrent.SharedExecutorPool.SHARED;
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Request>
{
@ -83,8 +92,43 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
"transport",
"Native-Transport-Auth-Requests");
/**
* Executor for handling requests from management connections (part of CEP-38).
*
* <p>Management connections are identified via Connection's flag set by the management
* transport server at initial connection setup. Request then are routed to a dedicated
* executor instead of the standard {@link #requestExecutor}. This provides isolation and
* prioritization of management operations, ensuring they can proceed even under
* a high load of regular client requests.
*
* <p>The executor is configured separately via
* {@link DatabaseDescriptor#getNativeTransportManagementMaxThreads()} to allow
* independent tuning of management operation throughput.
*
* <p>Management connections are established through the management transport server
* (see {@link org.apache.cassandra.service.NativeTransportManagementService}), which listens
* on a separate port from the regular native transport.
*
* <p>Unlike auth requests, management requests have no fallback executor, so values
* less than 1 are treated as 1: a zero-sized pool would queue management requests forever.
*/
@VisibleForTesting
static final LocalAwareExecutorPlus managementExecutor = SHARED.newExecutor(Math.max(1, DatabaseDescriptor.getNativeTransportManagementMaxThreads()),
DatabaseDescriptor::setNativeTransportManagementMaxThreads,
"transport",
"Native-Transport-Management-Tasks");
private static final ConcurrentMap<EventLoop, Flusher> flusherLookup = new ConcurrentHashMap<>();
/**
* Set while a management request is executing on the current thread. A management command may itself
* initiate the management server's stop (e.g. INVOKE COMMAND stopdaemon), in which case the drain-wait
* in {@link Server#close} runs on this very thread and must not wait for its own task to complete.
*/
private static final ThreadLocal<Boolean> IN_MANAGEMENT_TASK = ThreadLocal.withInitial(() -> Boolean.FALSE);
private final boolean useLegacyFlusher;
private final boolean isManagementDispatcher;
/**
* Takes a Channel, Request and the Response produced by processRequest and outputs a FlushItem
@ -98,9 +142,10 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
FlushItem<?> toFlushItem(P param, Channel channel, Message.Request request, Message.Response response);
}
public Dispatcher(boolean useLegacyFlusher)
public Dispatcher(boolean useLegacyFlusher, boolean isManagementDispatcher)
{
this.useLegacyFlusher = useLegacyFlusher;
this.isManagementDispatcher = isManagementDispatcher;
}
@Override
@ -123,10 +168,32 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
boolean isAuthQuery = DatabaseDescriptor.getNativeTransportMaxAuthThreads() > 0 &&
(request.type == Message.Type.AUTH_RESPONSE || request.type == Message.Type.CREDENTIALS);
// Importantly, the authExecutor will handle the AUTHENTICATE message which may be CPU intensive.
LocalAwareExecutorPlus executor = isAuthQuery ? authExecutor : requestExecutor;
if (isAuthQuery)
{
// Importantly, the authExecutor will handle the AUTHENTICATE message which may be CPU intensive.
authExecutor.submit(new RequestProcessor<>(channel, request, forFlusher, param, backpressure));
ClientMetrics.instance.markRequestDispatched();
return;
}
executor.submit(new RequestProcessor<>(channel, request, forFlusher, param, backpressure));
// Use connection object to check for management connections, this could be faster than checking
// channel attributies directly every time. For management connections, we route requests to
// the management executor.
Connection connection = request.connection();
if (connection instanceof ServerConnection)
{
ServerConnection serverConnection = (ServerConnection) connection;
if (serverConnection.isManagementConnection())
{
// Intentionally skipping ClientMetrics calls here: that meter tracks regular client request
// dispatch, and management API requests are accounted for separately dedicated metrics rather
// than mixing them into the client requests rate.
managementExecutor.submit(new ManagementRequestProcessor<>(channel, request, forFlusher, param, backpressure));
return;
}
}
requestExecutor.submit(new RequestProcessor<>(channel, request, forFlusher, param, backpressure));
ClientMetrics.instance.markRequestDispatched();
}
@ -293,13 +360,13 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
*/
public class RequestProcessor<P> implements DebuggableTask.RunnableDebuggableTask
{
private final Channel channel;
private final Message.Request request;
private final FlushItemConverter<P> forFlusher;
private final P flusherParam;
private final Overload backpressure;
protected final Channel channel;
protected final Message.Request request;
protected final FlushItemConverter<P> forFlusher;
protected final P flusherParam;
protected final Overload backpressure;
private volatile long startTimeNanos;
protected volatile long startTimeNanos;
public RequestProcessor(Channel channel, Message.Request request, FlushItemConverter<P> forFlusher, P flusherParam, Overload backpressure)
{
@ -345,6 +412,124 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
}
}
/** RequestProcessor for management connections that validates before executing. */
private class ManagementRequestProcessor<P> extends RequestProcessor<P> {
public ManagementRequestProcessor(Channel channel,
Message.Request request,
FlushItemConverter<P> forFlusher,
P flusherParam,
Overload backpressure) {
super(channel, request, forFlusher, flusherParam, backpressure);
}
@Override
public void run() {
startTimeNanos = MonotonicClock.Global.preciseTime.now();
RequestTime requestTime = new RequestTime(request.createdAtNanos, startTimeNanos);
// Validate management request BEFORE executing
Connection connection = request.connection();
if (connection instanceof ServerConnection) {
ServerConnection serverConnection = (ServerConnection) connection;
if (serverConnection.isManagementConnection()) {
if (!isManagementRequestAllowed(request)) {
// The flush pipeline takes the stream id from the request envelope, so the
// response must not carry one of its own.
Message.Response response = ErrorMessage.fromExceptionNoStreamId(
new InvalidRequestException(
"Only executions of the INVOKE COMMAND statements are allowed on the management port."));
response.attach(connection);
FlushItem<?> toFlush = forFlusher.toFlushItem(flusherParam, channel, request, response);
flush(toFlush);
return;
}
}
}
IN_MANAGEMENT_TASK.set(Boolean.TRUE);
try {
// If validation passes, call the normal processRequest to execute
// This calls the instance method processRequest() which does all the work
processRequest(channel, request, forFlusher, flusherParam, backpressure, requestTime);
} finally {
IN_MANAGEMENT_TASK.set(Boolean.FALSE);
}
}
}
@VisibleForTesting
static boolean isManagementRequestAllowed(Message.Request request)
{
switch (request.type)
{
case QUERY:
try
{
// Early parse the query to check if it's an INVOKE COMMAND statement.
// For management non-intensive operations double parsing is probably acceptable.
CQLStatement.Raw rawStatement = QueryProcessor.parseStatement(((QueryMessage) request).query);
if (rawStatement instanceof ExecuteCommandStatement.Raw)
return true;
// Allow read-only SELECT queries on system keyspaces (needed for driver metadata
// discovery), except system_auth (see isManagementReadableSystemKeyspace).
if (rawStatement instanceof SelectStatement.RawStatement)
{
SelectStatement.RawStatement selectRaw = (SelectStatement.RawStatement) rawStatement;
return selectRaw.isFullyQualified()
&& isManagementReadableSystemKeyspace(selectRaw.keyspace());
}
// This is also a corner case for the driver's behavior on the management port.
// When connecting, the driver sends a USE statement for the keyspace provided
// in driver.connect("system_schema").
if (rawStatement instanceof UseStatement)
{
UseStatement useStatement = (UseStatement) rawStatement;
return isManagementReadableSystemKeyspace(useStatement.keyspace());
}
return false;
}
catch (Exception e)
{
logger.warn("The command request parsing failed. The command will not be executed: {}", e.getMessage());
// If parsing fails (syntax error, etc.), it's not a valid command statement;
// this is expected for non-command queries.
return false;
}
case STARTUP:
case CREDENTIALS:
case AUTH_RESPONSE:
case OPTIONS:
case REGISTER:
return true; // Protocol messages are always allowed.
case EXECUTE:
case PREPARE:
case BATCH:
default:
return false; // Not supported and not allowed on management connections.
}
}
/**
* System keyspaces a CQL driver may read/USE on the management port for metadata discovery. This is
* {@link SchemaConstants#isSystemKeyspace(String)} (which also covers virtual system keyspaces) minus
* {@code system_auth}: that keyspace is the credential store (bcrypt {@code salted_hash}, superuser
* flags) and must never be readable over the management port, which is reachable without authorization
* (if AllowAllAuthorizer is enabled).
*/
private static boolean isManagementReadableSystemKeyspace(String keyspace)
{
if (keyspace == null)
return false;
if (SchemaConstants.AUTH_KEYSPACE_NAME.equals(toLowerCaseLocalized(keyspace)))
return false;
return SchemaConstants.isSystemKeyspace(keyspace)
|| SchemaConstants.isVirtualSystemKeyspace(keyspace);
}
/**
* Checks if the item in the head of the queue has spent more than allowed time in the queue.
*/
@ -355,7 +540,8 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
if (threshold <= 0)
return true;
return requestExecutor.oldestTaskQueueTime() < (DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS) * threshold);
LocalAwareExecutorPlus executor = isManagementDispatcher ? managementExecutor : requestExecutor;
return executor.oldestTaskQueueTime() < (DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS) * threshold);
}
/**
@ -500,15 +686,23 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
flusher.start();
}
/**
* @return true once the executor serving this dispatcher's server has drained: {@link #managementExecutor}
* for the management transport, {@link #requestExecutor} otherwise. A management task running on the
* calling thread is excluded it is the task that initiated the stop and cannot drain before it.
*/
public boolean isDone()
{
return requestExecutor.getPendingTaskCount() == 0 && requestExecutor.getActiveTaskCount() == 0;
LocalAwareExecutorPlus executor = isManagementDispatcher ? managementExecutor : requestExecutor;
int self = isManagementDispatcher && IN_MANAGEMENT_TASK.get() ? 1 : 0;
return executor.getPendingTaskCount() == 0 && executor.getActiveTaskCount() - self <= 0;
}
public static void shutdown()
{
requestExecutor.shutdown();
authExecutor.shutdown();
managementExecutor.shutdown();
}
/**

View File

@ -117,17 +117,20 @@ public class PipelineConfigurator
private final Dispatcher dispatcher;
// Shared between pre-v5 and CQLMessage handlers
private final QueueBackpressure queueBackpressure;
private final boolean isManagementConnection;
public PipelineConfigurator(boolean epoll,
boolean keepAlive,
EncryptionOptions.TlsEncryptionPolicy encryptionPolicy,
Dispatcher dispatcher)
Dispatcher dispatcher,
boolean isManagementConnection)
{
this.epoll = epoll;
this.keepAlive = keepAlive;
this.tlsEncryptionPolicy = encryptionPolicy;
this.dispatcher = dispatcher;
this.queueBackpressure = QueueBackpressure.DEFAULT;
this.queueBackpressure = isManagementConnection ? QueueBackpressure.MANAGEMENT_DEFAULT : QueueBackpressure.DEFAULT;
this.isManagementConnection = isManagementConnection;
}
@VisibleForTesting
@ -139,8 +142,9 @@ public class PipelineConfigurator
this.epoll = epoll;
this.keepAlive = keepAlive;
this.tlsEncryptionPolicy = encryptionPolicy;
this.dispatcher = new Dispatcher(useLegacyFlusher);
this.dispatcher = new Dispatcher(useLegacyFlusher, false);
this.queueBackpressure = QueueBackpressure.DEFAULT;
this.isManagementConnection = false;
}
public ChannelFuture initializeChannel(final EventLoopGroup workerGroup,
@ -261,6 +265,9 @@ public class PipelineConfigurator
pipeline.addFirst(CONNECTION_LIMIT_HANDLER, connectionLimitHandler);
}
if (isManagementConnection)
channel.attr(Connection.managementKey).set(Boolean.TRUE);
long idleTimeout = DatabaseDescriptor.nativeTransportIdleTimeout();
if (idleTimeout > 0)
{
@ -369,11 +376,6 @@ public class PipelineConfigurator
return new ClientResourceLimits.ResourceProvider.Default(allocator);
}
protected Dispatcher dispatcher(boolean useLegacyFlusher)
{
return new Dispatcher(useLegacyFlusher);
}
protected CQLMessageHandler.MessageConsumer<Message.Request> messageConsumer()
{
return dispatcher;

View File

@ -48,16 +48,35 @@ public interface QueueBackpressure
{
QueueBackpressure NO_OP = timeUnit -> 0;
QueueBackpressure DEFAULT = new QueueBackpressure()
{
private final AtomicReference<Incident> state = new AtomicReference<>(noBackpressure(() -> DatabaseDescriptor.getNativeTransportMinBackoffOnQueueOverload(TimeUnit.NANOSECONDS),
() -> DatabaseDescriptor.getNativeTransportMaxBackoffOnQueueOverload(TimeUnit.NANOSECONDS)));
/** Shared incident state for connections served by {@link Dispatcher#requestExecutor}. */
QueueBackpressure DEFAULT = newDefault();
public long markAndGetDelay(TimeUnit timeUnit)
/**
* Shared incident state for management connections ({@link Dispatcher#managementExecutor}). Kept separate
* from {@link #DEFAULT} because incident severity describes a single executor's queue: an overload streak
* on the client transport must not inflate delays applied to management connections, and vice versa.
*/
QueueBackpressure MANAGEMENT_DEFAULT = newDefault();
static QueueBackpressure newDefault()
{
return newDefault(() -> DatabaseDescriptor.getNativeTransportMinBackoffOnQueueOverload(TimeUnit.NANOSECONDS),
() -> DatabaseDescriptor.getNativeTransportMaxBackoffOnQueueOverload(TimeUnit.NANOSECONDS));
}
@VisibleForTesting
static QueueBackpressure newDefault(LongSupplier minDelayNanos, LongSupplier maxDelayNanos)
{
return new QueueBackpressure()
{
return state.updateAndGet(Incident::mark).delay(timeUnit);
}
};
private final AtomicReference<Incident> state = new AtomicReference<>(noBackpressure(minDelayNanos, maxDelayNanos));
public long markAndGetDelay(TimeUnit timeUnit)
{
return state.updateAndGet(Incident::mark).delay(timeUnit);
}
};
}
long markAndGetDelay(TimeUnit timeUnit);

View File

@ -85,7 +85,8 @@ public class Server implements CassandraDaemon.Server
{
public Connection newConnection(Channel channel, ProtocolVersion version)
{
return new ServerConnection(channel, version, connectionTracker);
boolean isManagementConnection = Boolean.TRUE.equals(channel.attr(Connection.managementKey).get());
return new ServerConnection(channel, version, connectionTracker, isManagementConnection);
}
};
@ -111,13 +112,14 @@ public class Server implements CassandraDaemon.Server
workerGroup = new NioEventLoopGroup();
}
dispatcher = new Dispatcher(DatabaseDescriptor.useNativeTransportLegacyFlusher());
dispatcher = new Dispatcher(DatabaseDescriptor.useNativeTransportLegacyFlusher(), builder.isManagementConnection);
pipelineConfigurator = builder.pipelineConfigurator != null
? builder.pipelineConfigurator
: new PipelineConfigurator(useEpoll,
DatabaseDescriptor.getRpcKeepAlive(),
builder.tlsEncryptionPolicy,
dispatcher);
dispatcher,
builder.isManagementConnection);
EventNotifier notifier = builder.eventNotifier != null ? builder.eventNotifier : new EventNotifier();
connectionTracker = new ConnectionTracker(isRunning::get);
@ -234,6 +236,7 @@ public class Server implements CassandraDaemon.Server
private InetSocketAddress socket;
private PipelineConfigurator pipelineConfigurator;
private EventNotifier eventNotifier;
private boolean isManagementConnection = false;
public Builder withTlsEncryptionPolicy(EncryptionOptions.TlsEncryptionPolicy tlsEncryptionPolicy)
{
@ -273,6 +276,12 @@ public class Server implements CassandraDaemon.Server
return this;
}
public Builder withManagementConnectionFlag(boolean management)
{
this.isManagementConnection = management;
return this;
}
public Server build()
{
return new Server(this);

View File

@ -43,13 +43,20 @@ public class ServerConnection extends Connection
private final ClientState clientState;
private volatile ConnectionStage stage;
public final Counter requests = new ThreadLocalCounter();
private final boolean isManagementConnection;
ServerConnection(Channel channel, ProtocolVersion version, Connection.Tracker tracker)
{
this(channel, version, tracker, false);
}
ServerConnection(Channel channel, ProtocolVersion version, Connection.Tracker tracker, boolean isManagementConnection)
{
super(channel, version, tracker);
clientState = ClientState.forExternalCalls(channel.remoteAddress());
stage = ConnectionStage.ESTABLISHED;
this.isManagementConnection = isManagementConnection;
}
public ClientState getClientState()
@ -158,4 +165,9 @@ public class ServerConnection extends Connection
.get("ssl");
return sslHandler != null;
}
public boolean isManagementConnection()
{
return isManagementConnection;
}
}

View File

@ -91,7 +91,7 @@ import static org.apache.cassandra.net.SocketFactory.newSslHandler;
import static org.apache.cassandra.transport.CQLMessageHandler.envelopeSize;
import static org.apache.cassandra.transport.Flusher.MAX_FRAMED_PAYLOAD_SIZE;
import static org.apache.cassandra.transport.PipelineConfigurator.SSL_FACTORY_CONTEXT_DESCRIPTION;
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
import static org.apache.cassandra.utils.concurrent.BlockingQueues.newBlockingQueue;
import static org.apache.cassandra.utils.concurrent.NonBlockingRateLimiter.NO_OP_LIMITER;
@ -100,6 +100,23 @@ public class SimpleClient implements Closeable
public static final int TIMEOUT_SECONDS = 10;
public static class TimeoutException extends RuntimeException
{
TimeoutException(long seconds)
{
super("Timed out after waiting " + seconds + " seconds for a server response. " +
"The request may still be executing on the server.");
}
}
public static class ConnectionClosedException extends RuntimeException
{
ConnectionClosedException()
{
super("Connection was closed by the server before a response was received.");
}
}
static
{
InternalLoggerFactory.setDefaultFactory(new Slf4JLoggerFactory());
@ -111,6 +128,8 @@ public class SimpleClient implements Closeable
public final int port;
private final EncryptionOptions.ClientEncryptionOptions encryptionOptions;
private final int largeMessageThreshold;
/** How long {@link #execute(Message.Request)} waits for a server response; {@code <= 0} means wait indefinitely. */
private final long requestTimeoutSeconds;
protected final ResponseHandler responseHandler = new ResponseHandler();
protected final Connection.Tracker tracker = new ConnectionTracker();
@ -131,6 +150,7 @@ public class SimpleClient implements Closeable
private ProtocolVersion version = ProtocolVersion.CURRENT;
private boolean useBeta = false;
private int largeMessageThreshold = FrameEncoder.Payload.MAX_SIZE;
private long requestTimeoutSeconds = TIMEOUT_SECONDS;
private Builder(String host, int port)
{
@ -162,6 +182,13 @@ public class SimpleClient implements Closeable
return this;
}
/** @param seconds response wait limit for each request; {@code <= 0} waits indefinitely */
public Builder requestTimeoutSeconds(long seconds)
{
requestTimeoutSeconds = seconds;
return this;
}
public SimpleClient build()
{
if (version.isBeta() && !useBeta)
@ -182,6 +209,7 @@ public class SimpleClient implements Closeable
this.version = builder.version;
this.encryptionOptions = builder.encryptionOptions.applyConfig();
this.largeMessageThreshold = builder.largeMessageThreshold;
this.requestTimeoutSeconds = builder.requestTimeoutSeconds;
}
public SimpleClient(String host, int port, ProtocolVersion version, EncryptionOptions.ClientEncryptionOptions encryptionOptions)
@ -211,6 +239,7 @@ public class SimpleClient implements Closeable
this.largeMessageThreshold = FrameEncoder.Payload.MAX_SIZE -
Math.max(FrameEncoderCrc.HEADER_AND_TRAILER_LENGTH,
FrameEncoderLZ4.HEADER_AND_TRAILER_LENGTH);
this.requestTimeoutSeconds = TIMEOUT_SECONDS;
}
public SimpleClient(String host, int port)
@ -328,9 +357,9 @@ public class SimpleClient implements Closeable
{
request.attach(connection);
lastWriteFuture = channel.writeAndFlush(Collections.singletonList(request));
Message.Response msg = responseHandler.responses.poll(TIMEOUT_SECONDS, TimeUnit.SECONDS);
Message.Response msg = awaitResponse(responseDeadlineNanos());
if (msg == null)
throw new RuntimeException("timeout");
throw new TimeoutException(requestTimeoutSeconds);
if (throwOnErrorResponse && msg instanceof ErrorMessage)
throw new RuntimeException((Throwable)((ErrorMessage)msg).error);
return msg;
@ -357,12 +386,12 @@ public class SimpleClient implements Closeable
}
lastWriteFuture = channel.writeAndFlush(requests);
long deadline = currentTimeMillis() + TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS);
long deadlineNanos = responseDeadlineNanos();
for (int i = 0; i < requests.size(); i++)
{
Message.Response msg = responseHandler.responses.poll(deadline - currentTimeMillis(), TimeUnit.MILLISECONDS);
Message.Response msg = awaitResponse(deadlineNanos);
if (msg == null)
throw new RuntimeException("timeout");
throw new TimeoutException(requestTimeoutSeconds);
if (msg instanceof ErrorMessage)
throw new RuntimeException((Throwable) ((ErrorMessage) msg).error);
rrMap.put(requests.get(msg.getSource().header.streamId), msg);
@ -395,6 +424,26 @@ public class SimpleClient implements Closeable
return source == null ? 0 : source.header.streamId;
}
private long responseDeadlineNanos()
{
return requestTimeoutSeconds > 0 ? nanoTime() + TimeUnit.SECONDS.toNanos(requestTimeoutSeconds) : Long.MAX_VALUE;
}
private Message.Response awaitResponse(long deadlineNanos) throws InterruptedException
{
while (true)
{
long waitNanos = Math.min(TimeUnit.SECONDS.toNanos(1), deadlineNanos - nanoTime());
Message.Response msg = waitNanos > 0 ? responseHandler.responses.poll(waitNanos, TimeUnit.NANOSECONDS) : null;
if (msg != null)
return msg;
if (responseHandler.connectionClosed)
throw new ConnectionClosedException();
if (nanoTime() - deadlineNanos >= 0)
return null;
}
}
public interface EventHandler
{
void onEvent(Event event);
@ -702,6 +751,14 @@ public class SimpleClient implements Closeable
{
public final BlockingQueue<Message.Response> responses = new SynchronousQueue<>(true);
public EventHandler eventHandler;
volatile boolean connectionClosed;
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception
{
connectionClosed = true;
ctx.fireChannelInactive();
}
@Override
public void channelRead0(ChannelHandlerContext ctx, Message.Response r)

View File

@ -21,6 +21,7 @@ import java.net.InetAddress;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
@ -37,6 +38,7 @@ import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.exceptions.CDCWriteException;
import org.apache.cassandra.exceptions.CasWriteTimeoutException;
import org.apache.cassandra.exceptions.CasWriteUnknownResultException;
import org.apache.cassandra.exceptions.CommandRequestExecutionException;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.ExceptionCode;
import org.apache.cassandra.exceptions.FunctionExecutionException;
@ -217,6 +219,18 @@ public class ErrorMessage extends Message.Response
int blockFor = body.readInt();
te = new CasWriteUnknownResultException(cl, received, blockFor);
break;
case COMMAND_FAILED:
if (version.isSmallerThan(ProtocolVersion.V5))
{
// Fallback for older clients - they'll get SERVER_ERROR instead
te = new ServerError(msg);
}
else
{
UUID executionId = CBUtil.readUUID(body);
te = new CommandRequestExecutionException(executionId, msg, null);
}
break;
}
return new ErrorMessage(te);
}
@ -302,6 +316,11 @@ public class ErrorMessage extends Message.Response
CBUtil.writeConsistencyLevel(cwue.consistency, dest);
dest.writeInt(cwue.received);
dest.writeInt(cwue.blockFor);
break;
case COMMAND_FAILED:
CommandRequestExecutionException cree = (CommandRequestExecutionException)err;
CBUtil.writeUUID(cree.getCommandExecutionId(), dest);
break;
}
}
@ -371,6 +390,10 @@ public class ErrorMessage extends Message.Response
CasWriteUnknownResultException cwue = (CasWriteUnknownResultException)err;
size += CBUtil.sizeOfConsistencyLevel(cwue.consistency) + 4 + 4; // receivedFor: 4, blockFor: 4
break;
case COMMAND_FAILED:
CommandRequestExecutionException cree = (CommandRequestExecutionException)err;
size += CBUtil.sizeOfUUID(cree.getCommandExecutionId()); // 16 bytes
break;
}
return size;
}
@ -408,6 +431,13 @@ public class ErrorMessage extends Message.Response
case CAS_WRITE_UNKNOWN:
CasWriteUnknownResultException cwue = (CasWriteUnknownResultException) msg.error;
return new WriteTimeoutException(WriteType.CAS, cwue.consistency, cwue.received, cwue.blockFor);
case COMMAND_FAILED:
// For older clients, the executionId is lost, but the message should contain it.
CommandRequestExecutionException cree = (CommandRequestExecutionException) msg.error;
String msgWithId = String.format("Command execution failed (executionId: %s): %s",
cree.getCommandExecutionId(),
cree.getMessage());
return new ServerError(msgWithId);
}
}

View File

@ -134,6 +134,12 @@ public class StartupMessage extends Message.Request
Guardrails.minimumClientDriverVersion.guard(driverName, driverVersion, clientState);
if (connection instanceof ServerConnection)
{
ServerConnection serverConnection = (ServerConnection) connection;
clientState.setManagement(serverConnection.isManagementConnection());
}
IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
if (authenticator.requireAuthentication())
{

View File

@ -259,4 +259,51 @@ public final class JsonUtils
valueMap.put(lowered, valueMap.remove(mapKey));
}
}
public static String getJsonType(Class<?> type)
{
if (type == String.class)
return "string";
else if (type == int.class || type == Integer.class)
return "integer";
else if (type == long.class || type == Long.class)
return "integer";
else if (type == boolean.class || type == Boolean.class)
return "boolean";
else if (type == double.class || type == Double.class ||
type == float.class || type == Float.class)
return "number";
else if (type.isArray() || List.class.isAssignableFrom(type))
return "array";
else if (Map.class.isAssignableFrom(type))
return "object";
else if (type.isEnum())
return "string";
else
return "string";
}
public static Object convertDefaultValue(String defaultValue, Class<?> type)
{
try
{
if (type == boolean.class || type == Boolean.class)
return Boolean.parseBoolean(defaultValue);
else if (type == int.class || type == Integer.class)
return Integer.parseInt(defaultValue);
else if (type == long.class || type == Long.class)
return Long.parseLong(defaultValue);
else if (type == double.class || type == Double.class)
return Double.parseDouble(defaultValue);
else if (type == float.class || type == Float.class)
return Float.parseFloat(defaultValue);
else
return defaultValue;
}
catch (Exception e)
{
// Fall back to string default value if parsing fails.
return defaultValue;
}
}
}

View File

@ -0,0 +1,20 @@
# 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.
#
#
org.apache.cassandra.management.picocli.PicocliCommandsProvider

View File

@ -118,6 +118,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.PathUtils;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.management.CommandInvokerService;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.metrics.Sampler;
import org.apache.cassandra.metrics.ThreadLocalMetrics;
@ -1044,6 +1045,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance
SnapshotManager.instance::close,
() -> IndexStatusManager.instance.shutdownAndWait(1L, MINUTES),
DiskErrorsHandlerService::close,
CommandInvokerService::shutdown,
() -> ThreadLocalMetrics.shutdownCleaner(1L, MINUTES)
);

View File

@ -19,79 +19,69 @@
package org.apache.cassandra.distributed.mock.nodetool;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.RuntimeMXBean;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Multimap;
import org.apache.cassandra.batchlog.BatchlogManager;
import org.apache.cassandra.batchlog.BatchlogManagerMBean;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.gms.FailureDetectorMBean;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.GossiperMBean;
import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.hints.HintsServiceMBean;
import org.apache.cassandra.locator.DynamicEndpointSnitch;
import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
import org.apache.cassandra.locator.EndpointSnitchInfo;
import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
import org.apache.cassandra.locator.SnitchAdapter;
import org.apache.cassandra.management.MBeanAccessor;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.MessagingServiceMBean;
import org.apache.cassandra.profiler.AsyncProfilerMBean;
import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.service.ActiveRepairServiceMBean;
import org.apache.cassandra.service.AsyncProfilerService;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.service.CacheServiceMBean;
import org.apache.cassandra.service.GCInspector;
import org.apache.cassandra.service.GCInspectorMXBean;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageProxyMBean;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.StorageServiceMBean;
import org.apache.cassandra.service.accord.AccordOperations;
import org.apache.cassandra.service.accord.AccordOperationsMBean;
import org.apache.cassandra.service.snapshot.SnapshotManager;
import org.apache.cassandra.service.snapshot.SnapshotManagerMBean;
import org.apache.cassandra.streaming.StreamManager;
import org.apache.cassandra.streaming.StreamManagerMBean;
import org.apache.cassandra.tcm.CMSOperations;
import org.apache.cassandra.tcm.CMSOperationsMBean;
import org.apache.cassandra.tools.NodeProbe;
public class InternalNodeProbe extends NodeProbe
{
private final boolean withNotifications;
private boolean previousSkipNotificationListeners = false;
public InternalNodeProbe(boolean withNotifications)
{
this.withNotifications = withNotifications;
connect();
}
protected void connect()
{
// note that we are not connecting via JMX for testing
mbeanServerConn = null;
jmxc = null;
super(new TestMockMBeanAccessor()); // host/port are unused in InternalNodeProbe
previousSkipNotificationListeners = StorageService.instance.skipNotificationListeners;
StorageService.instance.skipNotificationListeners = !withNotifications;
ssProxy = StorageService.instance;
snapshotProxy = SnapshotManager.instance;
cmsProxy = CMSOperations.instance;
accordProxy = AccordOperations.instance;
msProxy = MessagingService.instance();
streamProxy = StreamManager.instance;
compactionProxy = CompactionManager.instance;
fdProxy = (FailureDetectorMBean) FailureDetector.instance;
cacheService = CacheService.instance;
spProxy = StorageProxy.instance;
hsProxy = HintsService.instance;
gcProxy = new GCInspector();
gossProxy = Gossiper.instance;
bmProxy = BatchlogManager.instance;
arsProxy = ActiveRepairService.instance();
memProxy = ManagementFactory.getMemoryMXBean();
runtimeProxy = ManagementFactory.getRuntimeMXBean();
asyncProfilerProxy = AsyncProfilerService.instance();
}
@Override
@ -185,4 +175,78 @@ public class InternalNodeProbe extends NodeProbe
{
throw new UnsupportedOperationException();
}
private static class TestMockMBeanAccessor implements MBeanAccessor
{
private final Map<Class<?>, Object> mbeanRegistry = new HashMap<>();
public TestMockMBeanAccessor()
{
registerMBean(StorageServiceMBean.class, StorageService.instance);
registerMBean(SnapshotManagerMBean.class, SnapshotManager.instance);
registerMBean(CMSOperationsMBean.class, CMSOperations.instance);
registerMBean(AccordOperationsMBean.class, AccordOperations.instance);
registerMBean(MessagingServiceMBean.class, MessagingService.instance());
registerMBean(StreamManagerMBean.class, StreamManager.instance);
registerMBean(CompactionManagerMBean.class, CompactionManager.instance);
registerMBean(FailureDetectorMBean.class, (FailureDetectorMBean) FailureDetector.instance);
registerMBean(CacheServiceMBean.class, CacheService.instance);
registerMBean(StorageProxyMBean.class, StorageProxy.instance);
registerMBean(HintsServiceMBean.class, HintsService.instance);
registerMBean(GCInspectorMXBean.class, new GCInspector());
registerMBean(GossiperMBean.class, Gossiper.instance);
registerMBean(BatchlogManagerMBean.class, BatchlogManager.instance);
registerMBean(ActiveRepairServiceMBean.class, ActiveRepairService.instance());
registerMBean(MemoryMXBean.class, ManagementFactory.getMemoryMXBean());
registerMBean(RuntimeMXBean.class, ManagementFactory.getRuntimeMXBean());
registerMBean(AsyncProfilerMBean.class, AsyncProfilerService.instance());
}
protected <T> void registerMBean(Class<T> clazz, T mbean)
{
mbeanRegistry.put(clazz, mbean);
}
@Override
public <T> T findMBean(Class<T> clazz)
{
return mbeanRegistry.get(clazz) == null ? null : clazz.cast(mbeanRegistry.get(clazz));
}
@Override
public <T> T findMBeanMetric(Class<T> clazz, Props props)
{
return null;
}
@Override
public boolean isMBeanMetricRegistered(Props props)
{
return false;
}
@Override
public ColumnFamilyStoreMBean findColumnFamily(String type, String keyspace, String columnFamily)
{
return null;
}
@Override
public CompressionDictionaryManagerMBean findCompressionDictionary(String keyspace, String table)
{
return null;
}
@Override
public List<ThreadPoolInfo> threadPoolInfos()
{
return List.of();
}
@Override
public List<Map.Entry<String, ColumnFamilyStoreMBean>> findColumnFamilies(String type)
{
return List.of();
}
}
}

View File

@ -0,0 +1,172 @@
/*
* 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.distributed.test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Session;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import org.junit.Test;
import org.apache.cassandra.concurrent.SEPExecutor;
import org.apache.cassandra.concurrent.SharedExecutorPool;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.transport.messages.ResultMessage;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* This test verifyes that management requests are served even when native transport is under a heavy
* load by driver's SELECT requests. We block these requests and verify that management requests are
* still processed.
*/
public class ManagementRequestServingTest extends TestBaseImpl
{
private static final int MANAGEMENT_PORT = 11211;
private static final int NATIVE_PORT = 9042;
private static final int SELECT_REQUESTS_NUM = 10;
@Test
public void testManagementRequestsServedUnderRegularPortLoad() throws Exception
{
ExecutorService executor = Executors.newFixedThreadPool(SELECT_REQUESTS_NUM);
try (Cluster cluster = init(Cluster.build(1)
.withInstanceInitializer(BlockingSelectByteBuddy::install)
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)
.set("start_native_transport_management", true)
.set("native_transport_management_port", MANAGEMENT_PORT))
.start());
com.datastax.driver.core.Cluster driver = com.datastax.driver.core.Cluster.builder()
.addContactPoint("127.0.0.1")
.withPort(NATIVE_PORT)
.build();
Session session = driver.connect())
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int PRIMARY KEY, v int)"));
long mgmtBefore = cluster.get(1).callOnInstance(() -> getCompletedTaskCountByExecutor("Native-Transport-Management-Tasks"));
long reqBefore = cluster.get(1).callOnInstance(() -> getCompletedTaskCountByExecutor("Native-Transport-Requests"));
cluster.get(1).runOnInstance(() -> assertTrue(BlockingSelectByteBuddy.enabled.compareAndSet(false, true)));
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < SELECT_REQUESTS_NUM; i++)
futures.add(CompletableFuture.supplyAsync(() -> session.execute("SELECT * FROM " + KEYSPACE + ".tbl"), executor));
cluster.get(1).runOnInstance(() -> {
try
{
BlockingSelectByteBuddy.latch.await();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
});
try (com.datastax.driver.core.Cluster mgmt = com.datastax.driver.core.Cluster.builder()
.addContactPoint("127.0.0.1")
.withPort(MANAGEMENT_PORT)
.build();
Session mgmtSession = mgmt.connect("system"))
{
ResultSet rs = mgmtSession.execute("SELECT * FROM system.local");
assertFalse("Management query should return data", rs.all().isEmpty());
}
long mgmtAfter = cluster.get(1).callOnInstance(() -> getCompletedTaskCountByExecutor("Native-Transport-Management-Tasks"));
assertTrue("Management executor should have processed requests", mgmtAfter > mgmtBefore);
cluster.get(1).runOnInstance(() -> BlockingSelectByteBuddy.signal.countDown());
for (Future<?> f : futures)
f.get();
long reqAfter = cluster.get(1).callOnInstance(() -> getCompletedTaskCountByExecutor("Native-Transport-Requests"));
assertTrue("Request executor should have processed requests", reqAfter > reqBefore);
}
finally
{
executor.shutdown();
}
}
private static long getCompletedTaskCountByExecutor(String name)
{
return SharedExecutorPool.SHARED.executors.stream()
.filter(e -> e.name.endsWith(name))
.findFirst()
.map(SEPExecutor::getCompletedTaskCount)
.orElse(0L);
}
public static class BlockingSelectByteBuddy
{
static CountDownLatch latch = new CountDownLatch(SELECT_REQUESTS_NUM);
static CountDownLatch signal = new CountDownLatch(1);
static AtomicBoolean enabled = new AtomicBoolean(false);
static void install(ClassLoader cl, int ignored)
{
new ByteBuddy().rebase(SelectStatement.class)
.method(named("execute")
.and(takesArguments(QueryState.class, QueryOptions.class, Dispatcher.RequestTime.class)))
.intercept(MethodDelegation.to(BlockingSelectByteBuddy.class))
.make()
.load(cl, ClassLoadingStrategy.Default.INJECTION);
}
public static ResultMessage.Rows execute(QueryState state,
QueryOptions options,
Dispatcher.RequestTime requestTime,
@SuperCall Callable<ResultMessage.Rows> r) throws Exception
{
if (enabled.get() && !state.getClientState().isInternal && !state.getClientState().isManagement())
{
latch.countDown();
signal.await();
}
return r.call();
}
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.distributed.test;
import com.datastax.driver.core.Session;
import org.junit.Test;
import org.apache.cassandra.distributed.Cluster;
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class ManagementTransportMultiNodeTest extends TestBaseImpl
{
private static final int MANAGEMENT_PORT = 11211;
@Test
public void testMultiNodeManagementPort() throws Exception
{
int nodes = 3;
try (Cluster cluster = init(Cluster.build(nodes)
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)
.set("start_native_transport_management", true)
.set("native_transport_management_port", MANAGEMENT_PORT))
.start()))
{
for (int i = 1; i <= nodes; i++)
{
String host = cluster.get(i).config().broadcastAddress().getAddress().getHostAddress();
try (com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder()
.addContactPoint(host)
.withPort(MANAGEMENT_PORT)
.build();
Session s = c.connect())
{
assertFalse("Management port on node " + i + " should respond",
s.execute("SELECT * FROM system.local").all().isEmpty());
}
}
String node1Host = cluster.get(1).config().broadcastAddress().getAddress().getHostAddress();
try (com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder()
.addContactPoint(node1Host)
.withPort(MANAGEMENT_PORT)
.build();
Session s = c.connect())
{
int peerCount = s.execute("SELECT * FROM system.peers").all().size();
assertEquals("Node 1 should see other peers via management port",
nodes - 1, peerCount);
assertFalse("Keyspaces should be visible via management port",
s.execute("SELECT * FROM system_schema.keyspaces").all().isEmpty());
}
}
}
}

View File

@ -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.distributed.test;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.exceptions.InvalidQueryException;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.tools.ToolRunner;
import org.apache.cassandra.tools.ToolRunner.ToolResult;
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* End-to-end tests for the management native transport protocol (CEP-38).
*
* @see org.apache.cassandra.service.NativeTransportManagementService
*/
public class ManagementTransportProtocolTest extends TestBaseImpl
{
private static final Logger logger = LoggerFactory.getLogger(ManagementTransportProtocolTest.class);
private static final int MANAGEMENT_PORT = 11211;
private static final int NATIVE_PORT = 9042;
private static Cluster CLUSTER;
@BeforeClass
public static void setupCluster() throws Exception
{
CLUSTER = init(Cluster.build(1)
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)
.set("start_native_transport_management", true)
.set("native_transport_management_port", MANAGEMENT_PORT))
.start());
}
@AfterClass
public static void teardownCluster()
{
if (CLUSTER == null)
return;
CLUSTER.close();
}
@Test
public void testManagementPortRemainsUpWhenBinaryDisabled() throws Throwable
{
assertTrue("Regular port accessible", isAlive(NATIVE_PORT));
assertTrue("Management port accessible", isAlive(MANAGEMENT_PORT));
ToolResult tool = ToolRunner.invokeNodetoolJvmDtest(CLUSTER.get(1), "disablebinary");
assertEquals(0, tool.getExitCode());
assertFalse("Regular port NOT accessible after disable", isAlive(NATIVE_PORT));
assertTrue("Management port still accessible after disable", isAlive(MANAGEMENT_PORT));
tool = ToolRunner.invokeNodetoolJvmDtest(CLUSTER.get(1), "enablebinary");
assertEquals(0, tool.getExitCode());
assertTrue("Regular port accessible after re-enable", isAlive(NATIVE_PORT));
}
@Test
public void testSelectOnSystemKeyspacesAllowed()
{
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
Session s = c.connect())
{
assertFalse(s.execute("SELECT * FROM system.local").all().isEmpty());
// system.peers is empty on single-node
assertTrue(s.execute("SELECT * FROM system.peers").all().isEmpty());
assertFalse(s.execute("SELECT * FROM system_schema.keyspaces").all().isEmpty());
assertFalse(s.execute("SELECT * FROM system_schema.tables").all().isEmpty());
assertFalse(s.execute("SELECT * FROM system_schema.columns").all().isEmpty());
}
}
@Test
public void testSelectOnVirtualKeyspacesAllowed()
{
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
Session s = c.connect())
{
s.execute("SELECT * FROM system_virtual_schema.keyspaces");
s.execute("SELECT * FROM system_virtual_schema.tables");
}
}
@Test(expected = InvalidQueryException.class)
public void testSelectOnUserKeyspaceRejected()
{
CLUSTER.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".mgmt_test (pk int PRIMARY KEY, v int)");
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
Session s = c.connect())
{
s.execute("SELECT * FROM " + KEYSPACE + ".mgmt_test");
}
}
@Test(expected = InvalidQueryException.class)
public void testInsertRejectedOnManagementPort()
{
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
Session s = c.connect())
{
s.execute("INSERT INTO system.local (key) VALUES ('test')");
}
}
@Test(expected = InvalidQueryException.class)
public void testDDLRejectedOnManagementPort()
{
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
Session s = c.connect())
{
s.execute("CREATE TABLE " + KEYSPACE + ".should_fail (pk int PRIMARY KEY)");
}
}
@Test(expected = InvalidQueryException.class)
public void testUnqualifiedSelectRejected()
{
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
Session s = c.connect())
{
s.execute("SELECT * FROM local");
}
}
/**
* This is also a corner case for the driver's behavior on the management port.
* When connecting, the driver sends a USE statement for the keyspace provided in connect().
* If that keyspace is a system keyspace, the USE msut succeeds, and subsequent queries without
* keyspace qualification are allowed. If that keyspace is a user keyspace, the USE should fail.
*/
@Test
public void testConnectWithSystemKeyspaceAllowed()
{
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
Session s = c.connect("system_schema"))
{
// Driver sends USE system_schema, then we query a table without qualification
assertFalse(s.execute("SELECT * FROM system_schema.keyspaces").all().isEmpty());
}
}
@Test
public void testConnectWithUserKeyspaceRejected()
{
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
Session s = c.connect(KEYSPACE))
{
fail("Should not be able to USE a user keyspace on management port");
}
catch (Exception e)
{
logger.debug("Expected failure when connecting with user keyspace: {}", e.getMessage());
}
}
private static com.datastax.driver.core.Cluster driverOnPort(int port)
{
return com.datastax.driver.core.Cluster.builder()
.addContactPoint("127.0.0.1")
.withPort(port)
.build();
}
private static boolean isAlive(int port)
{
try (com.datastax.driver.core.Cluster c = driverOnPort(port);
Session s = c.connect())
{
s.execute("SELECT * FROM system.local");
return true;
}
catch (Exception e)
{
return false;
}
}
}

View File

@ -18,13 +18,10 @@
package org.apache.cassandra.distributed.test;
import java.io.IOException;
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.Session;
import org.assertj.core.api.Assertions;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.distributed.api.ICluster;
@ -46,52 +43,96 @@ import static org.junit.Assert.assertTrue;
*/
public class NodeToolEnableDisableBinaryTest extends TestBaseImpl
{
private static ICluster cluster;
@Before
public void setupEnv() throws IOException
{
if (cluster == null)
{
cluster = init(builder().withNodes(1)
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL))
.start());
cluster.get(1).nodetool("disableautocompaction");
}
}
@AfterClass
public static void teardownEnv() throws Exception
{
cluster.close();
}
@Test
public void testEnableDisableBinary() throws Throwable
{
// We can connect
assertTrue(canConnect());
try (ICluster<?> nodeCluster = init(builder().withNodes(1)
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL))
.start()))
{
nodeCluster.get(1).nodetool("disableautocompaction");
// We can't connect after disabling
ToolResult tool = ToolRunner.invokeNodetoolJvmDtest(cluster.get(1), "disablebinary");
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Stop listening for CQL clients");
assertTrue(tool.getCleanedStderr().isEmpty());
assertEquals(0, tool.getExitCode());
assertFalse(canConnect());
// We can connect
assertTrue(canConnect());
// We can connect after re-enabling
tool = ToolRunner.invokeNodetoolJvmDtest(cluster.get(1), "enablebinary");
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Starting listening for CQL clients");
assertTrue(tool.getCleanedStderr().isEmpty());
assertEquals(0, tool.getExitCode());
assertTrue(canConnect());
// We can't connect after disabling
ToolResult tool = ToolRunner.invokeNodetoolJvmDtest(nodeCluster.get(1), "disablebinary");
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Stop listening for CQL clients");
assertTrue(tool.getCleanedStderr().isEmpty());
assertEquals(0, tool.getExitCode());
assertFalse(canConnect());
// We can connect after re-enabling
tool = ToolRunner.invokeNodetoolJvmDtest(nodeCluster.get(1), "enablebinary");
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Starting listening for CQL clients");
assertTrue(tool.getCleanedStderr().isEmpty());
assertEquals(0, tool.getExitCode());
assertTrue(canConnect());
}
}
/**
* This is an important corner case to verify that when binary protocol is disabled,
* we can still connect to the management port and query system tables. This case
* must be explicitly tested because the management port is used by ops and other
* monitoring tools to control the cluster.
*
* @throws Throwable If any exception occurs during the test.
*/
@Test
public void testSystemQueriesViaManagementPortWhenBinaryDisabled() throws Throwable
{
int managementPort = 11211;
int numberOfNodes = 1;
try (ICluster<?> managementCluster = init(builder().withNodes(numberOfNodes)
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)
.set("start_native_transport_management", true)
.set("native_transport_management_port", managementPort))
.start()))
{
managementCluster.get(1).nodetool("disableautocompaction");
assertTrue("Regular native CQL port should is accessible", canConnect());
assertTrue("Management port should is accessible", canConnect(managementPort));
ToolResult tool = ToolRunner.invokeNodetoolJvmDtest(managementCluster.get(1), "disablebinary");
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Stop listening for CQL clients");
assertEquals(0, tool.getExitCode());
assertFalse("Regular native CQL port should NOT be accessible", canConnect());
assertTrue("Management port should is accessible", canConnect(managementPort));
try (Cluster c = Cluster.builder()
.addContactPoint("127.0.0.1")
.withPort(managementPort)
.build();
Session s = c.connect())
{
assertFalse("system.local should return data",
s.execute("SELECT * FROM system.local").all().isEmpty());
assertTrue("system.peers is NOT empty, howeverwe only have one node in the cluster",
s.execute("SELECT * FROM system.peers").all().isEmpty());
assertFalse("system_schema.keyspaces should return data",
s.execute("SELECT * FROM system_schema.keyspaces").all().isEmpty());
assertFalse("system_schema.tables should return data",
s.execute("SELECT * FROM system_schema.tables").all().isEmpty());
}
tool = ToolRunner.invokeNodetoolJvmDtest(managementCluster.get(1), "enablebinary");
assertEquals(0, tool.getExitCode());
assertTrue("Should connect to regular binary after re-enabling", canConnect());
}
}
private boolean canConnect()
{
return canConnect(9042);
}
private boolean canConnect(int port)
{
boolean canConnect = false;
try(com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder()
.addContactPoint("127.0.0.1")
.withPort(port)
.build();
Session s = c.connect("system_schema"))
{

Some files were not shown because too many files have changed in this diff Show More