use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use cheetah_string::CheetahString;
use dashmap::DashMap;
use parking_lot::RwLock;
use rocketmq_observability::metrics::client::ClientMetrics;
use rocketmq_observability::TelemetryHandle;
use rocketmq_protocol::protocol::remoting_command_defaults::RemotingCommandDefaults;
use rocketmq_protocol::protocol::remoting_command_defaults::RemotingCommandFactory;
use rocketmq_runtime::ChildServiceContext;
use rocketmq_runtime::ResourceBudget;
use rocketmq_runtime::ShutdownDeadline;
use rocketmq_transport::api::RPCHook;
use tracing::info;
use crate::base::client_config::ClientConfig;
use crate::base::client_options::ClientOptions;
use crate::factory::mq_client_instance::MQClientInstance;
use crate::nameserver_discovery::NameServerDiscoveryConfig;
use crate::producer::produce_accumulator::ProduceAccumulator;
use crate::producer::request_future_holder::RequestFutureHolder;
struct ClientPoolEntry {
generation: u64,
leases: usize,
identity: ClientPoolIdentity,
instance: Arc<MQClientInstance>,
}
#[derive(Clone, Eq, PartialEq)]
struct ClientPoolIdentity {
namesrv_addr: Option<CheetahString>,
discovery_fingerprint: Option<CheetahString>,
use_tls: bool,
vip_channel_enabled: bool,
api_timeout_millis: u64,
rpc_hook_identity: Option<usize>,
remoting_command_defaults: RemotingCommandDefaults,
}
impl ClientPoolIdentity {
fn new(
client_config: &ClientConfig,
discovery: Option<&NameServerDiscoveryConfig>,
rpc_hook: Option<&Arc<dyn RPCHook>>,
remoting_command_factory: RemotingCommandFactory,
) -> Self {
Self {
namesrv_addr: client_config.namesrv_addr.clone(),
discovery_fingerprint: discovery.map(|config| CheetahString::from_string(config.fingerprint())),
use_tls: client_config.use_tls,
vip_channel_enabled: client_config.vip_channel_enabled,
api_timeout_millis: client_config.mq_client_api_timeout,
rpc_hook_identity: rpc_hook.map(|hook| Arc::as_ptr(hook).cast::<()>() as usize),
remoting_command_defaults: remoting_command_factory.defaults(),
}
}
}
type ClientInstanceHashMap = DashMap<CheetahString , ClientPoolEntry>;
type AccumulatorHashMap = DashMap<CheetahString , Arc<ProduceAccumulator>>;
#[derive(Clone)]
pub struct ClientPool {
inner: Arc<ClientPoolInner>,
}
struct ClientPoolInner {
service_context: ChildServiceContext,
resource_budget: ResourceBudget,
telemetry_handle: TelemetryHandle,
client_metrics: ClientMetrics,
factory_table: Arc<ClientInstanceHashMap>,
accumulator_table: Arc<AccumulatorHashMap>,
request_future_holder: Arc<RequestFutureHolder>,
remoting_command_factory: RemotingCommandFactory,
next_generation: AtomicU64,
closed: AtomicBool,
admission: RwLock<()>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClientPoolToken {
client_id: CheetahString,
generation: u64,
}
pub struct PooledClient {
instance: Arc<MQClientInstance>,
token: ClientPoolToken,
}
impl PooledClient {
pub fn instance(&self) -> &Arc<MQClientInstance> {
&self.instance
}
pub fn into_parts(self) -> (Arc<MQClientInstance>, ClientPoolToken) {
(self.instance, self.token)
}
}
impl ClientPool {
pub(crate) fn new(
service_context: ChildServiceContext,
resource_budget: ResourceBudget,
telemetry_handle: TelemetryHandle,
client_metrics: ClientMetrics,
remoting_command_factory: RemotingCommandFactory,
) -> Self {
let request_future_holder = Arc::new(RequestFutureHolder::new(service_context.component("request-futures")));
Self {
inner: Arc::new(ClientPoolInner {
service_context,
resource_budget,
telemetry_handle,
client_metrics,
factory_table: Arc::new(DashMap::with_capacity(128)),
accumulator_table: Arc::new(DashMap::with_capacity(128)),
request_future_holder,
remoting_command_factory,
next_generation: AtomicU64::new(0),
closed: AtomicBool::new(false),
admission: RwLock::new(()),
}),
}
}
pub fn instance_count(&self) -> usize {
self.inner.factory_table.len()
}
pub(crate) fn request_future_holder(&self) -> Arc<RequestFutureHolder> {
Arc::clone(&self.inner.request_future_holder)
}
pub fn get_or_create(
&self,
client_config: ClientConfig,
rpc_hook: Option<Arc<dyn RPCHook>>,
) -> crate::ClientResult<PooledClient> {
self.get_or_create_with_options(ClientOptions::legacy(client_config), rpc_hook)
}
pub fn get_or_create_with_options(
&self,
options: ClientOptions,
rpc_hook: Option<Arc<dyn RPCHook>>,
) -> crate::ClientResult<PooledClient> {
let _admission = self.inner.admission.read();
if self.inner.closed.load(Ordering::Acquire) {
return Err(mq_client_err!("ClientRuntime is shutting down"));
}
let (client_config, discovery, command_factory_override) = options.into_normalized_parts()?;
let remoting_command_factory = command_factory_override.unwrap_or(self.inner.remoting_command_factory);
let client_id = CheetahString::from_string(client_config.build_mq_client_id());
let identity = ClientPoolIdentity::new(
&client_config,
discovery.as_ref(),
rpc_hook.as_ref(),
remoting_command_factory,
);
let mut entry = self.inner.factory_table.entry(client_id.clone()).or_insert_with(|| {
let generation = self.inner.next_generation.fetch_add(1, Ordering::AcqRel) + 1;
info!(
client_id = %client_id,
generation,
"Created new MQClientInstance in ClientPool"
);
let instance = MQClientInstance::new_arc_with_resource_budget_and_discovery(
client_config,
discovery,
remoting_command_factory,
generation,
client_id.clone(),
rpc_hook,
self.inner.service_context.component(
rocketmq_runtime::ScopeId::try_new(format!("instance-{generation}"))
.expect("the client instance scope has a fixed nonblank prefix"),
),
Arc::clone(&self.inner.request_future_holder),
self.inner.resource_budget.clone(),
self.inner.telemetry_handle.clone(),
self.inner.client_metrics.clone(),
);
ClientPoolEntry {
generation,
leases: 0,
identity: identity.clone(),
instance,
}
});
if entry.identity != identity {
return Err(mq_client_err!(
"ClientPool configuration conflicts with an existing client-id owner"
));
}
entry.leases += 1;
Ok(PooledClient {
instance: Arc::clone(&entry.instance),
token: ClientPoolToken {
client_id,
generation: entry.generation,
},
})
}
pub fn get_or_create_produce_accumulator(&self, client_config: ClientConfig) -> Option<Arc<ProduceAccumulator>> {
let _admission = self.inner.admission.read();
if self.inner.closed.load(Ordering::Acquire) {
return None;
}
let client_id = CheetahString::from_string(client_config.build_mq_client_id());
Some(
self.inner
.accumulator_table
.entry(client_id.clone())
.or_insert_with(|| {
info!("Created new ProduceAccumulator for clientId:[{}]", client_id);
Arc::new(ProduceAccumulator::with_resource_budget(
self.inner.service_context.component(
rocketmq_runtime::ScopeId::try_new(format!("accumulator-{}", client_id))
.expect("the accumulator scope has a fixed nonblank prefix"),
),
client_id.as_str(),
self.inner.resource_budget.clone(),
))
})
.clone(),
)
}
pub async fn release(&self, token: ClientPoolToken) -> bool {
let should_remove = self
.inner
.factory_table
.get_mut(&token.client_id)
.is_some_and(|mut current| {
if current.generation != token.generation || current.leases == 0 {
return false;
}
current.leases -= 1;
current.leases == 0
});
if !should_remove {
return false;
}
let removed = self
.inner
.factory_table
.remove_if(&token.client_id, |_, current| {
current.generation == token.generation && current.leases == 0
})
.map(|(_, entry)| entry.instance);
if let Some(instance) = removed {
instance.shutdown().await;
true
} else {
false
}
}
pub(crate) async fn shutdown_until(&self, deadline: ShutdownDeadline) {
let instances = {
let _admission = self.inner.admission.write();
self.inner.closed.store(true, Ordering::Release);
let keys = self
.inner
.factory_table
.iter()
.map(|entry| entry.key().clone())
.collect::<Vec<_>>();
let mut instances = Vec::with_capacity(keys.len());
for key in keys {
if let Some((_, entry)) = self.inner.factory_table.remove(&key) {
instances.push(entry.instance);
}
}
self.inner.accumulator_table.clear();
instances
};
for instance in instances {
if deadline.is_expired() {
break;
}
let _ = tokio::time::timeout(deadline.remaining(), instance.shutdown()).await;
}
}
}
#[cfg(test)]
mod tests {
use crate::test_support::error_assertions::client_exception;
use cheetah_string::CheetahString;
use rocketmq_protocol::protocol::SerializeType;
use super::*;
#[test]
fn invalid_nameserver_address_is_rejected_before_pool_admission() {
let runtime = crate::runtime::test_client_runtime("invalid-nameserver-pool-test");
let config = ClientConfig {
namesrv_addr: Some(CheetahString::from_static_str("missing-port")),
..Default::default()
};
let error = match runtime.pool().get_or_create(config, None) {
Ok(_) => panic!("invalid NameServer address must be rejected"),
Err(error) => error,
};
assert!(error.is(&rocketmq_error::CORE_CONFIGURATION_INVALID));
assert_eq!(runtime.pool().instance_count(), 0);
}
#[test]
fn canonical_nameserver_address_drives_pool_identity() {
let runtime = crate::runtime::test_client_runtime("canonical-nameserver-pool-test");
let first_config = ClientConfig {
namesrv_addr: Some(CheetahString::from_static_str(" NS-A:9876 ; ns-a:9876 ")),
..Default::default()
};
let first = runtime
.pool()
.get_or_create(first_config, None)
.expect("first pooled client");
let equivalent_config = ClientConfig {
namesrv_addr: Some(CheetahString::from_static_str("ns-a:9876")),
..Default::default()
};
let equivalent = runtime
.pool()
.get_or_create(equivalent_config, None)
.expect("equivalent canonical target must share the pool entry");
assert!(Arc::ptr_eq(first.instance(), equivalent.instance()));
let different_config = ClientConfig {
namesrv_addr: Some(CheetahString::from_static_str("ns-b:9876")),
..Default::default()
};
assert!(runtime.pool().get_or_create(different_config, None).is_err());
}
#[test]
fn remoting_defaults_participate_in_pool_identity() {
let runtime = crate::runtime::test_client_runtime("remoting-defaults-pool-identity-test");
let mut config = ClientConfig::default();
config.set_instance_name("remoting-defaults-owner".into());
let json = RemotingCommandFactory::new(RemotingCommandDefaults::new(501, SerializeType::JSON));
let binary = RemotingCommandFactory::new(RemotingCommandDefaults::new(502, SerializeType::ROCKETMQ));
runtime
.pool()
.get_or_create_with_options(
ClientOptions::legacy(config.clone()).with_remoting_command_factory(json),
None,
)
.expect("first command owner");
let error = match runtime.pool().get_or_create_with_options(
ClientOptions::legacy(config).with_remoting_command_factory(binary),
None,
) {
Ok(_) => panic!("different remoting defaults must not share one client-id owner"),
Err(error) => error,
};
assert!(client_exception(&error)
.to_string()
.contains("ClientPool configuration conflicts with an existing client-id owner"));
}
}