#
# This is ExplainDB, educational database systems materials.
#
# Copyright (C) 2026 Prof. Dr. Jens Dittrich, Saarland University
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
#
"""RAID reliability and performance cost model for various RAID levels."""
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from statistics import mean
from typing import Iterator
[docs]
class ResilienceScenario(Enum):
"""Resilience scenarios for a storage subsystem. This is used to model the resilience of a storage subsystem
against errors."""
# any single subsystem may fail, and we can still recover all data:
ANY_SINGLE_SUBSYSTEM_MAY_FAIL = 1
# all but one subsystem may fail, and we can still recover all data:
ALL_BUT_ONE_SUBSYSTEM_MAY_FAIL = 42
def __str__(self):
"""Render as the bare member name (e.g. ``ANY_SINGLE_SUBSYSTEM_MAY_FAIL``)
rather than the default ``ResilienceScenario.<name>`` form."""
return self.name
[docs]
class SubSystem(ABC):
"""A cost model for an abstract subsystem used to model the performance of storage devices. This can be anything
from a single chip of an SSD to a whole RAID subsystem. The subsystem has to provide information about its performance
characteristics.
"""
def __init__(self):
pass
[docs]
@abstractmethod
def get_storage_blow_up(self) -> float:
"""Returns the storage blow-up in terms of the extra storage space introduced by the subsystem. This means,
if we return 1.1 this means we add 10% more storage space to the original storage space, 1.0 means no extra
storage space is added.
"""
[docs]
@abstractmethod
def get_resilience_scenario(
self,
) -> tuple[ResilienceScenario, Iterator[SubSystem] | None] | None:
"""Returns the scenario this SubSystem is resilient against, i.e. this scenario may happen, still
this subsystem instance can recover all data."""
[docs]
def get_resilience_scenarios(
self, rec_depth: int = 0
) -> Iterator[
tuple[int, SubSystem, ResilienceScenario, Iterator[SubSystem] | None]
]:
"""Returns an Iterator over all resilience scenarios recursively.
Each tuple contains:
0. the subsystem instance,
1. the resilience scenario for that instance, and
2. an iterator over the children subsystems that may fail or None.
"""
# return the resilience scenario of this subsystem:
resilience_scenario: (
tuple[ResilienceScenario, Iterator[SubSystem] | None] | None
) = self.get_resilience_scenario()
if resilience_scenario is not None:
yield rec_depth, self, *resilience_scenario
def __str__(self):
"""Render as the concrete subsystem's class name."""
return f"{self.__class__.__name__}"
[docs]
class SubSystemArray(SubSystem, ABC):
"""A cost model for an abstract subsystem keeping a list of references to nested subsystems."""
def __init__(self, subsystems: list[SubSystem]):
"""Create the array over the given nested subsystems.
@param subsystems: The subsystems aggregated by this array.
"""
super().__init__()
self.subsystems: list[SubSystem] = subsystems
[docs]
def get_resilience_scenarios(
self, rec_depth: int = 0
) -> Iterator[
tuple[int, SubSystem, ResilienceScenario, Iterator[SubSystem] | None]
]:
"""Returns an Iterator over all resilience scenarios of the subsystems in this subsystem recursively.
Each tuple contains:
0. the subsystem instance,
1. the resilience scenario for that instance, and
2. an iterator over the children subsystems that may fail or None.
"""
# return the resilience scenario of this subsystem:
yield from super().get_resilience_scenarios(rec_depth=rec_depth)
# return the resilience scenarios of all children subsystems:
subsystem: SubSystem
for subsystem in self.subsystems:
yield from subsystem.get_resilience_scenarios(rec_depth=rec_depth + 1)
[docs]
class Device(SubSystem):
"""A device in a storage subsystem. This class must provide information about its performance characteristics."""
def __init__(
self,
sequential_read_performance: int,
sequential_write_performance: int,
read_IO_performance: int,
write_IO_performance: int,
storage_blow_up: float = 1,
):
"""Create a device from its measured performance characteristics.
@param sequential_read_performance: Sequential read throughput in MB/s.
@param sequential_write_performance: Sequential write throughput in MB/s.
@param read_IO_performance: Random read I/O operations per second (IOPs).
@param write_IO_performance: Random write I/O operations per second (IOPs).
@param storage_blow_up: Storage blow-up factor (1.0 = no overhead, 1.1 =
10% extra, < 1.0 = space saved, e.g. via compression).
"""
super().__init__()
self.sequential_read_performance: int = sequential_read_performance
self.sequential_write_performance: int = sequential_write_performance
self.read_IO_performance: int = read_IO_performance
self.write_IO_performance: int = write_IO_performance
self.storage_blow_up: float = storage_blow_up
[docs]
def get_storage_blow_up(self) -> float:
"""Returns the storage blow-up in terms of the extra storage space introduced by the subsystem. This means,
if we return 1.1 this means we add 10% more storage space to the original storage space, 1.0 means no extra
storage space is added. A value < 1.0 means we reduce the storage space, i.e. by using compression.
"""
return self.storage_blow_up
[docs]
def get_resilience_scenario(
self,
) -> tuple[ResilienceScenario, Iterator[SubSystem] | None] | None:
"""A device is not resilient against any failure."""
return None
[docs]
class RAID_0(SubSystemArray):
"""RAID 0: Striping without parity"""
[docs]
def get_storage_blow_up(self) -> float:
"""Returns the storage blow-up in terms of the extra storage space introduced by the subsystem. This means,
if we return 1.1 this means we add 10% more storage space to the original storage space, 1.0 means no extra
storage space is added. A value < 1.0 means we reduce the storage space, i.e. by using compression.
"""
return 1.0 * mean(
subsystem.get_storage_blow_up() for subsystem in self.subsystems
)
[docs]
def get_resilience_scenario(
self,
) -> tuple[ResilienceScenario, Iterator[SubSystem] | None]:
"""RAID 0 is not resilient against any failure."""
[docs]
class RAID_1(SubSystemArray):
"""RAID 1: Mirroring"""
[docs]
def get_storage_blow_up(self) -> float:
"""Returns the storage blow-up in terms of the extra storage space introduced by the subsystem. This means,
if we return 1.1 this means we add 10% more storage space to the original storage space, 1.0 means no extra
storage space is added. A value < 1.0 means we reduce the storage space, i.e. by using compression.
"""
return sum(subsystem.get_storage_blow_up() for subsystem in self.subsystems)
[docs]
def get_resilience_scenario(
self,
) -> tuple[ResilienceScenario, Iterator[SubSystem] | None] | None:
"""RAID 1 is resilient against an error where all but one subsystem fails."""
return ResilienceScenario.ALL_BUT_ONE_SUBSYSTEM_MAY_FAIL, iter(self.subsystems)
[docs]
class RAID_5(SubSystemArray):
"""RAID 5: Striping with distributed (round-robin) parity"""
def __init__(self, subsystems: list[SubSystem]):
"""See :meth:`SubSystemArray.__init__`.
RAID 5 additionally requires at least three subsystems and asserts this.
"""
super().__init__(subsystems)
assert len(subsystems) >= 3, "RAID 5 needs at least 3 subsystems."
[docs]
def get_storage_blow_up(self) -> float:
"""Returns the storage blow-up in terms of the extra storage space introduced by the subsystem. This means,
if we return 1.1 this means we add 10% more storage space to the original storage space, 1.0 means no extra
storage space is added. A value < 1.0 means we reduce the storage space, i.e. by using compression.
"""
mean_storage_blow_up: float = mean(
subsystem.get_storage_blow_up() for subsystem in self.subsystems
)
return mean_storage_blow_up * len(self.subsystems) / (len(self.subsystems) - 1)
[docs]
def get_resilience_scenario(
self,
) -> tuple[ResilienceScenario, Iterator[SubSystem] | None] | None:
"""RAID 5 is resilient against a single subsystem failure."""
return ResilienceScenario.ANY_SINGLE_SUBSYSTEM_MAY_FAIL, iter(self.subsystems)