Source code for swh.objstorage.backends.winery.pools

# Copyright (C) 2021-2026  The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information

import logging
import os
from pathlib import Path
import shlex
import stat
import subprocess
from types import TracebackType
from typing import List, Literal, Optional, Protocol

from .. import settings

logger = logging.getLogger(__name__)


[docs] class ImageReader(Protocol): """ Protocol for images' reader classes. This allows a pool to pick a class depending on the backing file format (currently swh.shard.Shard or swh.mosaic.MosaicReader) """
[docs] def lookup(self, key: bytes) -> bytes | None: ...
[docs] def close(self) -> None: ...
[docs] class ImageWriter(Protocol): """ Protocol for images' writer classes. This allows a pool to pick a class depending on the backing file format (currently swh.shard.ShardCreator or swh.mosaic.MosaicWriter). """ def __enter__(self) -> "ImageWriter": ... def __exit__( self, exc_type: Optional[BaseException], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> bool: ...
[docs] def write(self, key: bytes, object: bytes) -> None: ...
[docs] class Pool(Protocol): pool_name: str
[docs] def image_exists(self, image: str) -> bool: """Check whether the named image exists (it does not have to be mapped)""" ...
[docs] def image_mapped(self, image: str) -> Optional[Literal["ro", "rw"]]: """Check whether the image is already mapped, read-only or read-write""" imgpath = self.image_path(image) if os.access(imgpath, os.R_OK): if os.access(imgpath, os.W_OK): return "rw" return "ro" return None
[docs] def image_list(self) -> List[str]: """List all known images, mapped or not""" ...
[docs] def image_path(self, image: str) -> str: """Return a path to the image, that can be opened with :func:`open`.""" ...
[docs] def image_create(self, image: str) -> None: """Create a new image named `image` and allocate the right amount of space.""" ...
[docs] def image_map(self, image: str, options: str) -> None: """Map an image for use. Options can be `"ro"` to map the image read-only, or `"rw"` to map the image read-write.""" ...
[docs] def image_unmap(self, image: str) -> None: """Unmap the image. Once this is done, the image is unavailable for use.""" ...
[docs] def image_remap_ro(self, image: str): self.image_unmap(image) self.image_map(image, "ro")
[docs] def image_import(self, image: str) -> None: """Import an existing image file in the current pool""" ...
[docs] def image_open(self, image: str) -> ImageReader: ...
[docs] def delete_object(self, shard_name: str, obj_id: bytes) -> None: ...
[docs] def open_writer( self, shard_name: str, nb_objects: int, xcreate_image: bool ) -> ImageWriter: """Instantiate the correct ImageWriter object for the given shard This is used at time of packing a batch of objects in a shard file. """ ...
@staticmethod def _zero_image_if_needed(path): """Check whether the image is empty, and zero it out if it's not. We really check only the first 1kB, as we assume that the SWHShard marker will have been written at the beginning of the image under all circumstances if the RO Shard creation has been interrupted. """ with open(path, "rb") as f: start = f.read(1024) if not start or set(start) == {0}: return logger.warning("RO image %s isn't empty, cleaning it up", path) st = os.stat(path) if stat.S_ISBLK(st.st_mode): # Block device, use DISCARD command = ["/usr/sbin/blkdiscard", path] else: # Regular file, use fallocate --punch-hole command = [ "/usr/bin/fallocate", "--punch-hole", "-l", str(st.st_size), path, ] try: subprocess.run(command, check=True, capture_output=True) except subprocess.CalledProcessError: logger.warning("%s failed:", shlex.join(command), path, exc_info=True)
[docs] class FileBackedPool(Pool): """File-backed pool for Winery shards mimicking a Ceph RBD pool. Unmapped images are represented by setting the file permission to 0o000. """ def __init__( self, base_directory: Path, pool_name: str, shard_max_size: int, ) -> None: self.base_directory = base_directory self.pool_name = pool_name self.image_size = shard_max_size self.pool_dir = self.base_directory / self.pool_name self.pool_dir.mkdir(exist_ok=True)
[docs] def image_exists(self, image: str) -> bool: return (self.pool_dir / image).is_file()
[docs] def image_list(self) -> List[str]: return [entry.name for entry in self.pool_dir.iterdir() if entry.is_file()]
[docs] def image_path(self, image: str) -> str: return str(self.pool_dir / image)
[docs] def image_create(self, image: str) -> None: path = self.image_path(image) if os.path.exists(path): if os.stat(path).st_mode == 0o100600: # If the image exists but is -rw------- it is expected to be a # dandling/stale shard file left by a crashed/aborted packing # process logger.warning("Stale image found. Reusing it") else: raise ValueError(f"Image {image} already exists") open(path, "w").close() self.image_map(image, "rw")
[docs] def image_map(self, image: str, options: str) -> None: if "ro" in options: os.chmod(self.image_path(image), 0o400) else: os.chmod(self.image_path(image), 0o600)
[docs] def image_unmap(self, image: str) -> None: os.chmod(self.image_path(image), 0o000)
[docs] def image_unmap_all(self) -> None: for entry in self.pool_dir.iterdir(): if entry.is_file(): entry.chmod(0o000)
[docs] def image_import(self, image: str) -> None: name = os.path.basename(image) dst = self.image_path(name) os.link(image, dst) # ensure image_mapped()=="ro" os.chmod(dst, 0o400)
[docs] def pool_from_settings( shards_settings: settings.Shards, shards_pool_settings: settings.ShardsPool, ) -> Pool: """Return a Pool from the settings""" pool_type = shards_pool_settings["type"] if pool_type == "rbd": from .rbd import RBDPool rbd_settings = settings.rbd_shards_pool_settings_with_defaults( shards_pool_settings ) return RBDPool( shard_max_size=shards_settings["max_size"], rbd_use_sudo=rbd_settings["use_sudo"], rbd_pool_name=rbd_settings["pool_name"], rbd_data_pool_name=rbd_settings["data_pool_name"], rbd_image_features_unsupported=rbd_settings["image_features_unsupported"], rbd_map_options=rbd_settings["map_options"], ) elif pool_type == "directory": from .shard import ShardBackedPool dir_settings = settings.directory_shards_pool_settings_with_defaults( shards_pool_settings ) return ShardBackedPool( shard_max_size=shards_settings["max_size"], base_directory=Path(dir_settings["base_directory"]), pool_name=dir_settings["pool_name"], ) else: raise ValueError(f"Unknown shards pool type: {pool_type}")