blob: 4de7cee9f38278134e574e944623ef9773669aa0 [file]
// 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.
//! One-stop construction for an [`IggyShard`] plus, on shard 0 only, its
//! associated [`ShardZeroCoordinator`].
//!
//! The coordinator owns shard-0's round-robin state for replica and
//! client placement. Cluster-wide `replica_id -> owning_shard` routing
//! flows through the cluster-shared [`message_bus::ReplicaOwnerTable`],
//! stamped by the owning shard's installer and CAS-cleared on disconnect.
//!
//! The coordinator is constructed before the shard and passed to
//! [`IggyShard::new`] as a ctor argument, so an `IggyShard` is fully
//! wired the instant it becomes observable to any reader.
use crate::coordinator::{ShardZeroCoordinator, classify_try_send_err};
use crate::metrics::{ShardMetrics, frame_drop_variant};
use crate::{
CoordinatorConfig, IggyShard, LifecycleFrame, ListClientsHandler, MetadataSubmitHandler,
PartitionConsensusConfig, PartitionReadHandler, Receiver, ShardCtorError, ShardFrame,
ShardIdentity, TaggedSender,
};
use consensus::VsrConsensus;
use journal::JournalHandle;
use message_bus::client_listener::RequestHandler;
use message_bus::replica::listener::MessageHandler;
use message_bus::{MessageBus, SendError};
use metadata::IggyMetadata;
use metadata::stm::StateMachine;
use partitions::IggyPartitions;
use std::rc::Rc;
use crate::shards_table::ShardsTable;
/// A freshly constructed [`IggyShard`].
pub struct BuiltShard<B, MJ, S, M, T>
where
B: MessageBus,
{
pub shard: IggyShard<B, MJ, S, M, T>,
}
/// Builder that pairs [`IggyShard`] construction with coordinator wiring
/// on shard 0. Non-zero shards skip the coordinator entirely; the
/// `coord_config` field is then ignored.
pub struct IggyShardBuilder<B, MJ, S, M, T>
where
B: MessageBus,
{
identity: ShardIdentity,
bus: B,
on_replica_message: MessageHandler,
on_client_request: RequestHandler,
on_metadata_submit: MetadataSubmitHandler,
on_list_clients: ListClientsHandler,
on_partition_read: PartitionReadHandler,
metadata: IggyMetadata<VsrConsensus<B>, MJ, S, M>,
partitions: IggyPartitions<B>,
senders: Vec<TaggedSender>,
inbox: Receiver<ShardFrame>,
shards_table: T,
partition_consensus: PartitionConsensusConfig<B>,
coord_config: CoordinatorConfig,
metrics: ShardMetrics,
}
impl<B, MJ, S, M, T> IggyShardBuilder<B, MJ, S, M, T>
where
B: MessageBus + Clone + 'static,
T: ShardsTable,
MJ: JournalHandle,
S: Send + 'static,
M: StateMachine,
{
/// Create a builder carrying every input needed by both
/// [`IggyShard::new`] and (for shard 0) `ShardZeroCoordinator::new`.
#[allow(clippy::too_many_arguments)]
pub fn new(
identity: ShardIdentity,
bus: B,
on_replica_message: MessageHandler,
on_client_request: RequestHandler,
on_metadata_submit: MetadataSubmitHandler,
on_list_clients: ListClientsHandler,
on_partition_read: PartitionReadHandler,
metadata: IggyMetadata<VsrConsensus<B>, MJ, S, M>,
partitions: IggyPartitions<B>,
senders: Vec<TaggedSender>,
inbox: Receiver<ShardFrame>,
shards_table: T,
partition_consensus: PartitionConsensusConfig<B>,
coord_config: CoordinatorConfig,
metrics: ShardMetrics,
) -> Self {
Self {
identity,
bus,
on_replica_message,
on_client_request,
on_metadata_submit,
on_list_clients,
on_partition_read,
metadata,
partitions,
senders,
inbox,
shards_table,
partition_consensus,
coord_config,
metrics,
}
}
/// Consume the builder and produce a fully wired [`BuiltShard`]. On
/// shard 0 this constructs the coordinator and passes it to the shard
/// ctor. On any other shard the coordinator-specific fields are
/// dropped.
///
/// # Errors
///
/// Returns [`ShardCtorError::SenderOrderingInvalid`] if `senders` is
/// not in canonical order (`senders[i].shard_id() != i`) and
/// [`ShardCtorError::ShardCountOverflow`] if `senders.len()` does not
/// fit in `u16`. Both are bootstrap programming errors and the
/// `u16` overflow check fires on every shard, not only shard 0.
pub fn build(self) -> Result<BuiltShard<B, MJ, S, M, T>, ShardCtorError> {
let is_shard_zero = self.identity.id == 0;
// Fail fast before installing forward closures: a misordered
// senders vec would silently misroute every cross-shard frame.
crate::validate_sender_ordering(&self.senders)?;
let total_shards =
u16::try_from(self.senders.len()).map_err(|_| ShardCtorError::ShardCountOverflow {
count: self.senders.len(),
})?;
// Install the cross-shard forward closures. The bus' slow path
// (`send_to_replica` / `send_to_client`) calls these when the
// destination connection lives on a different shard; the closure
// wraps the message into a `LifecycleFrame::Forward{Replica,
// Client}Send` and `try_send`s it to the owning shard's inbox.
// The receiving shard's router unwraps the frame and re-enters
// the bus' fast path locally. The `senders` Vec is canonically
// ordered (`senders[i].shard_id() == i`).
let senders_for_replica: Rc<Vec<TaggedSender>> = Rc::new(self.senders.clone());
let senders_for_client = Rc::clone(&senders_for_replica);
let metrics_for_replica = self.metrics.clone();
let metrics_for_client = self.metrics.clone();
self.bus
.set_replica_forward_fn(Box::new(move |replica_id, owning_shard, msg| {
senders_for_replica.get(usize::from(owning_shard)).map_or(
Err(SendError::ReplicaForwardFailed(replica_id)),
|sender| {
let frame = ShardFrame::lifecycle(LifecycleFrame::ForwardReplicaSend {
replica_id,
msg,
});
sender.try_send(frame).map_err(|err| {
metrics_for_replica.record_frame_drop(
frame_drop_variant::FORWARD_REPLICA_SEND,
classify_try_send_err(&err),
);
SendError::ReplicaForwardFailed(replica_id)
})
},
)
}));
self.bus
.set_client_forward_fn(Box::new(move |client_id, owning_shard, msg| {
senders_for_client.get(usize::from(owning_shard)).map_or(
Err(SendError::ClientForwardFailed(client_id)),
|sender| {
let frame = ShardFrame::lifecycle(LifecycleFrame::ForwardClientSend {
client_id,
msg,
});
sender.try_send(frame).map_err(|err| {
metrics_for_client.record_frame_drop(
frame_drop_variant::FORWARD_CLIENT_SEND,
classify_try_send_err(&err),
);
SendError::ClientForwardFailed(client_id)
})
},
)
}));
// On shard 0: build the coordinator BEFORE the shard, so the
// coordinator can be passed to `IggyShard::new` as a ctor arg.
let coordinator = if is_shard_zero {
let coord_senders: Vec<TaggedSender> = self.senders.clone();
Some(Rc::new(ShardZeroCoordinator::new(
Rc::new(coord_senders),
total_shards,
self.coord_config.clone(),
self.metrics.clone(),
)?))
} else {
None
};
let shard = IggyShard::new(
self.identity,
self.bus,
self.on_replica_message,
self.on_client_request,
self.on_metadata_submit,
self.on_list_clients,
self.on_partition_read,
self.metadata,
self.partitions,
self.senders,
self.inbox,
self.shards_table,
self.partition_consensus,
coordinator,
self.metrics,
)?;
Ok(BuiltShard { shard })
}
}