blob: 633dc22809e67d55ba30f39b18a9193c53b34cd2 [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.
use crate::cache::AllocationStrategy;
use iggy_common::TcpSender;
use rand::{SeedableRng, rngs::StdRng, seq::SliceRandom};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
rc::Rc,
};
const MAX_CONNECTIONS_PER_REPLICA: usize = 8;
// TODO: Move to some common trait location.
pub trait ShardedState {
type Entry;
type Delta;
fn apply(&mut self, delta: Self::Delta);
}
/// Least-loaded allocation strategy for connections
#[derive(Debug)]
pub struct LeastLoadedStrategy {
total_shards: usize,
connections_per_shard: RefCell<Vec<(u16, usize)>>,
replica_to_shards: RefCell<HashMap<u8, HashSet<u16>>>,
rng_seed: u64,
}
impl LeastLoadedStrategy {
pub fn new(total_shards: usize, seed: u64) -> Self {
Self {
total_shards,
connections_per_shard: RefCell::new((0..total_shards).map(|s| (s as u16, 0)).collect()),
replica_to_shards: RefCell::new(HashMap::new()),
rng_seed: seed,
}
}
fn create_shard_mappings(
&self,
mappings: &mut Vec<ShardAssignment>,
replica: u8,
mut conn_shards: Vec<u16>,
) {
for shard in &conn_shards {
mappings.push(ShardAssignment {
replica,
shard: *shard,
conn_shard: *shard,
});
}
let mut rng = StdRng::seed_from_u64(self.rng_seed);
conn_shards.shuffle(&mut rng);
let mut j = 0;
for shard in 0..self.total_shards {
let shard = shard as u16;
if conn_shards.contains(&shard) {
continue;
}
let conn_idx = j % conn_shards.len();
mappings.push(ShardAssignment {
replica,
shard,
conn_shard: conn_shards[conn_idx],
});
j += 1;
}
}
}
/// Identifies a connection on a specific shard
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConnectionAssignment {
replica: u8,
shard: u16,
}
/// Maps a source shard to the shard that owns the connection
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShardAssignment {
replica: u8,
shard: u16,
conn_shard: u16,
}
/// Changeset for connection-based allocation
#[derive(Debug, Clone)]
pub enum ConnectionChanges {
Allocate {
connections: Vec<ConnectionAssignment>,
mappings: Vec<ShardAssignment>,
},
Deallocate {
connections: Vec<ConnectionAssignment>,
mappings: Vec<ConnectionAssignment>,
},
}
type Delta = <ConnectionCache as ShardedState>::Delta;
impl AllocationStrategy<ConnectionCache> for LeastLoadedStrategy {
fn allocate(&self, replica: u8) -> Option<Delta> {
if self.replica_to_shards.borrow().contains_key(&replica) {
return None;
}
let mut connections = Vec::new();
let mut mappings = Vec::new();
let connections_needed = self.total_shards.min(MAX_CONNECTIONS_PER_REPLICA);
let mut rng = StdRng::seed_from_u64(self.rng_seed);
self.connections_per_shard.borrow_mut().shuffle(&mut rng);
self.connections_per_shard
.borrow_mut()
.sort_by_key(|(_, count)| *count);
let mut assigned_shards = HashSet::with_capacity(connections_needed);
for i in 0..connections_needed {
let mut connections_per_shard = self.connections_per_shard.borrow_mut();
let (shard, count) = connections_per_shard.get_mut(i).unwrap();
connections.push(ConnectionAssignment {
replica,
shard: *shard,
});
*count += 1;
assigned_shards.insert(*shard);
}
self.replica_to_shards
.borrow_mut()
.insert(replica, assigned_shards.clone());
self.create_shard_mappings(
&mut mappings,
replica,
assigned_shards.into_iter().collect(),
);
Some(Delta::Allocate {
connections,
mappings,
})
}
fn deallocate(&self, replica: u8) -> Option<Delta> {
let conn_shards = self.replica_to_shards.borrow_mut().remove(&replica)?;
let mut connections = Vec::new();
let mut mappings = Vec::new();
for shard in &conn_shards {
if let Some((_, count)) = self
.connections_per_shard
.borrow_mut()
.iter_mut()
.find(|(s, _)| s == shard)
{
*count = count.saturating_sub(1);
}
connections.push(ConnectionAssignment {
replica,
shard: *shard,
});
}
for shard in 0..self.total_shards {
let shard = shard as u16;
mappings.push(ConnectionAssignment { replica, shard });
}
Some(Delta::Deallocate {
connections,
mappings,
})
}
}
/// Coordinator that wraps a strategy for a specific sharded state type
#[derive(Debug)]
pub struct Coordinator<A, SS>
where
SS: ShardedState,
A: AllocationStrategy<SS>,
{
strategy: A,
_ss: std::marker::PhantomData<SS>,
}
impl<A, SS> Coordinator<A, SS>
where
SS: ShardedState,
A: AllocationStrategy<SS>,
{
pub fn new(strategy: A) -> Self {
Self {
strategy,
_ss: std::marker::PhantomData,
}
}
pub fn allocate(&self, entry: SS::Entry) -> Option<SS::Delta> {
self.strategy.allocate(entry)
}
pub fn deallocate(&self, entry: SS::Entry) -> Option<SS::Delta> {
self.strategy.deallocate(entry)
}
}
#[derive(Debug)]
pub struct ShardedConnections<A, SS>
where
SS: ShardedState,
A: AllocationStrategy<SS>,
{
pub coordinator: Coordinator<A, SS>,
pub state: SS,
}
impl<A, SS> ShardedConnections<A, SS>
where
SS: ShardedState,
A: AllocationStrategy<SS>,
{
pub fn allocate(&mut self, entry: SS::Entry) -> bool {
if let Some(delta) = self.coordinator.allocate(entry) {
// TODO: broadcast to other shards.
self.state.apply(delta);
true
} else {
false
}
}
pub fn deallocate(&mut self, entry: SS::Entry) -> bool {
if let Some(delta) = self.coordinator.deallocate(entry) {
// TODO: broadcast to other shards.
self.state.apply(delta);
true
} else {
false
}
}
}
/// Cache for connection state per shard
#[derive(Debug, Default)]
pub struct ConnectionCache {
pub shard_id: u16,
pub connections: HashMap<u8, Option<Rc<TcpSender>>>,
pub connection_map: HashMap<u8, u16>,
}
impl ConnectionCache {
pub fn get_connection(&self, replica: u8) -> Option<Rc<TcpSender>> {
self.connections.get(&replica).and_then(|opt| opt.clone())
}
pub fn get_mapped_shard(&self, replica: u8) -> Option<u16> {
self.connection_map.get(&replica).copied()
}
}
impl ShardedState for ConnectionCache {
type Entry = u8; // replica id
type Delta = ConnectionChanges;
fn apply(&mut self, delta: Self::Delta) {
let shard_id = self.shard_id;
match delta {
ConnectionChanges::Allocate {
connections,
mappings,
} => {
for conn in connections.iter().filter(|c| c.shard == shard_id) {
self.connections.insert(conn.replica, None);
}
for mapping in &mappings {
self.connection_map
.insert(mapping.replica, mapping.conn_shard);
}
}
ConnectionChanges::Deallocate {
connections,
mappings,
} => {
for conn in connections.iter().filter(|c| c.shard == shard_id) {
self.connections.remove(&conn.replica);
}
for mapping in &mappings {
self.connection_map.remove(&mapping.replica);
}
}
}
}
}