forked from mooncake-track/Mooncake
Compare commits
69 Commits
main
...
P2P-Moonca
| Author | SHA1 | Date |
|---|---|---|
|
|
036075ccfa | |
|
|
aad5111781 | |
|
|
188bb41121 | |
|
|
29e2c7e873 | |
|
|
0d96064ee0 | |
|
|
b21f90d032 | |
|
|
90f2005b55 | |
|
|
6fd613e370 | |
|
|
b0214baf0e | |
|
|
84a823b9eb | |
|
|
6ab0016ed3 | |
|
|
5f2a814eb9 | |
|
|
9cd7a12bcc | |
|
|
99fb4d4af6 | |
|
|
0d8a51f417 | |
|
|
46206e777e | |
|
|
4aa6209f8f | |
|
|
62ef96e53b | |
|
|
5e746c1f01 | |
|
|
f036f3d266 | |
|
|
9acd5689a0 | |
|
|
48d3a47cdd | |
|
|
80942798a3 | |
|
|
b363dc9073 | |
|
|
034dc124d6 | |
|
|
de863db9de | |
|
|
e4f4128faf | |
|
|
d335215b0b | |
|
|
591bb9edcc | |
|
|
d8918d509e | |
|
|
1717e928ae | |
|
|
2163ab20ed | |
|
|
899a46e07a | |
|
|
73a0585bb4 | |
|
|
7d38918fa8 | |
|
|
b7957ee2f1 | |
|
|
95e36ea5e3 | |
|
|
39af9953af | |
|
|
e9c82477d3 | |
|
|
dd94ad25e7 | |
|
|
949c9f6bf0 | |
|
|
254e51aab9 | |
|
|
9d9028b467 | |
|
|
7ca1b447ae | |
|
|
d31ebffe95 | |
|
|
ca16744df1 | |
|
|
3bb98aae29 | |
|
|
e27a814878 | |
|
|
bd71efe05a | |
|
|
751d5f6122 | |
|
|
90db6b6199 | |
|
|
80f5659348 | |
|
|
81d673979c | |
|
|
b40bb82830 | |
|
|
925fb18b11 | |
|
|
6691a9856a | |
|
|
fee0642df6 | |
|
|
8e85cd4881 | |
|
|
6ec3581c3f | |
|
|
6fb4a84e6e | |
|
|
4af9267051 | |
|
|
45c5f2c62d | |
|
|
58110cfe09 | |
|
|
2c8ffdc20e | |
|
|
b4977b916b | |
|
|
880c5eff3a | |
|
|
4632f72fe9 | |
|
|
509b554da0 | |
|
|
3beb69812b |
|
|
@ -25,5 +25,6 @@
|
|||
## Checklist
|
||||
|
||||
- [ ] I have performed a self-review of my own code.
|
||||
- [ ] I have formatted my own code using `./scripts/code_format.sh` before submitting.
|
||||
- [ ] I have updated the documentation.
|
||||
- [ ] I have added tests to prove my changes are effective.
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ name: 'Build & Test (Linux)'
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: [ "main" , "P2P-Mooncake-Store"]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: [ "main" , "P2P-Mooncake-Store"]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
|
||||
|
|
@ -70,17 +70,18 @@ jobs:
|
|||
- name: Configure project
|
||||
run: |
|
||||
sudo apt update -y
|
||||
sudo apt install -y ninja-build
|
||||
sudo bash -x dependencies.sh -y
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DUSE_HTTP=ON -DUSE_ETCD=ON -DSTORE_USE_ETCD=ON -DENABLE_ASAN=ON -DENABLE_SCCACHE=ON
|
||||
cmake -G Ninja .. -DUSE_HTTP=ON -DUSE_CXL=ON -DUSE_ETCD=ON -DSTORE_USE_ETCD=ON -DENABLE_ASAN=ON -DENABLE_SCCACHE=ON -DCMAKE_BUILD_TYPE=Debug
|
||||
shell: bash
|
||||
|
||||
- name: Build project
|
||||
run: |
|
||||
cd build
|
||||
make -j
|
||||
sudo make install
|
||||
cmake --build .
|
||||
sudo cmake --install .
|
||||
shell: bash
|
||||
|
||||
- name: Build nvlink_allocator.so
|
||||
|
|
@ -103,7 +104,7 @@ jobs:
|
|||
cd build
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
|
||||
ldconfig -v || echo "always continue"
|
||||
MC_METADATA_SERVER=http://127.0.0.1:8080/metadata DEFAULT_KV_LEASE_TTL=500 make test -j ARGS="-V"
|
||||
MC_METADATA_SERVER=http://127.0.0.1:8080/metadata DEFAULT_KV_LEASE_TTL=500 ctest -j --output-on-failure
|
||||
shell: bash
|
||||
|
||||
- name: Generate Python version tag
|
||||
|
|
@ -135,18 +136,19 @@ jobs:
|
|||
- name: Configure project
|
||||
run: |
|
||||
apt update -y
|
||||
apt install -y ninja-build
|
||||
bash -x dependencies.sh -y
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DUSE_MUSA=ON -DUSE_MNNVL=ON -DUSE_ETCD=ON -DSTORE_USE_ETCD=ON -DUSE_CXL=ON -DUSE_TCP=ON -DBUILD_UNIT_TESTS=OFF -DBUILD_EXAMPLES=OFF
|
||||
cmake -G Ninja .. -DUSE_MUSA=ON -DUSE_MNNVL=ON -DUSE_ETCD=ON -DSTORE_USE_ETCD=ON -DUSE_CXL=ON -DUSE_TCP=ON -DBUILD_UNIT_TESTS=OFF -DBUILD_EXAMPLES=OFF
|
||||
shell: bash
|
||||
|
||||
- name: Build project
|
||||
run: |
|
||||
cd build
|
||||
source ~/.bashrc
|
||||
make -j
|
||||
make install
|
||||
cmake --build .
|
||||
cmake --install .
|
||||
shell: bash
|
||||
|
||||
test-wheel-ubuntu:
|
||||
|
|
@ -238,7 +240,7 @@ jobs:
|
|||
LOCAL_HOSTNAME: "127.0.0.1"
|
||||
run: |
|
||||
source test_env/bin/activate
|
||||
python scripts/test_tensor_api.py -n 1
|
||||
python scripts/test_tensor_api.py --mode perf --iterations 1
|
||||
shell: bash
|
||||
|
||||
- name: Run RPC Communicator Bandwidth Test
|
||||
|
|
@ -251,17 +253,6 @@ jobs:
|
|||
kill $SERVER_PID 2>/dev/null || true
|
||||
wait $SERVER_PID 2>/dev/null || true
|
||||
|
||||
- name: Test Mooncake EP Backend (CPU Only)
|
||||
env:
|
||||
MC_FORCE_TCP: "true"
|
||||
run: |
|
||||
source test_env/bin/activate
|
||||
python -m unittest mooncake-wheel.tests.test_mooncake_backend_cpu
|
||||
# Disable these tests in CI as they fail occasionally.
|
||||
# python -m unittest mooncake-wheel.tests.test_mooncake_backend_elastic
|
||||
# python -m unittest mooncake-wheel.tests.test_mooncake_backend_p2p_cpu
|
||||
shell: bash
|
||||
|
||||
test-sglang-integration:
|
||||
needs: build-flags
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -329,7 +320,7 @@ jobs:
|
|||
python-version: ['3.10', '3.12']
|
||||
env:
|
||||
BUILD_WITH_EP: "1"
|
||||
EP_TORCH_VERSIONS: "2.8.0;2.9.0;2.9.1"
|
||||
EP_TORCH_VERSIONS: "2.9.0;2.9.1;2.10.0"
|
||||
TORCH_CUDA_ARCH_LIST: "8.0;9.0"
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
|
||||
|
|
@ -375,6 +366,7 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update -y
|
||||
sudo apt install -y ninja-build
|
||||
sudo bash -x dependencies.sh -y
|
||||
df -h
|
||||
shell: bash
|
||||
|
|
@ -386,9 +378,9 @@ jobs:
|
|||
cd build
|
||||
export PATH=/usr/local/nvidia/bin:/usr/local/nvidia/lib64:$PATH
|
||||
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
|
||||
cmake .. -DUSE_ETCD=OFF -DUSE_REDIS=ON -DUSE_HTTP=ON -DWITH_METRICS=ON -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON -DUSE_CUDA=OFF -DUSE_MNNVL=OFF -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
|
||||
make -j4
|
||||
sudo make install
|
||||
cmake -G Ninja .. -DUSE_ETCD=OFF -DUSE_CXL=ON -DUSE_REDIS=ON -DUSE_HTTP=ON -DWITH_METRICS=ON -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON -DUSE_CUDA=OFF -DUSE_MNNVL=OFF -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
|
||||
cmake --build .
|
||||
sudo cmake --install .
|
||||
df -h
|
||||
shell: bash
|
||||
|
||||
|
|
@ -396,7 +388,7 @@ jobs:
|
|||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DUSE_ETCD=ON -DUSE_REDIS=ON -DUSE_HTTP=ON -DWITH_STORE=ON -DWITH_P2P_STORE=ON -DWITH_EP=ON -DWITH_METRICS=ON -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON -DUSE_CUDA=ON -DUSE_MNNVL=OFF -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
|
||||
cmake -G Ninja .. -DUSE_ETCD=ON -DUSE_CXL=ON -DUSE_REDIS=ON -DUSE_HTTP=ON -DWITH_STORE=ON -DWITH_P2P_STORE=ON -DWITH_METRICS=ON -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON -DUSE_CUDA=ON -DUSE_MNNVL=OFF -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
|
||||
shell: bash
|
||||
# TODO: lack USE_NVMEOF,USE_MNNVL
|
||||
|
||||
|
|
@ -405,15 +397,15 @@ jobs:
|
|||
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
|
||||
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
|
||||
cd build
|
||||
make -j4
|
||||
sudo make install
|
||||
cmake --build .
|
||||
sudo cmake --install .
|
||||
df -h
|
||||
shell: bash
|
||||
|
||||
- name: Configure project with unit tests and examples
|
||||
run: |
|
||||
cd build
|
||||
cmake .. -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON
|
||||
cmake -G Ninja .. -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON
|
||||
shell: bash
|
||||
# TODO: lack WITH_RUST_EXAMPLE
|
||||
|
||||
|
|
@ -422,15 +414,15 @@ jobs:
|
|||
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
|
||||
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
|
||||
cd build
|
||||
make -j4
|
||||
sudo make install
|
||||
cmake --build .
|
||||
sudo cmake --install .
|
||||
shell: bash
|
||||
|
||||
- name: Configure project
|
||||
run: |
|
||||
cd build
|
||||
rm -r */tests
|
||||
cmake .. -DBUILD_UNIT_TESTS=OFF -DBUILD_EXAMPLES=OFF -DUSE_HTTP=ON -DENABLE_SCCACHE=ON
|
||||
cmake -G Ninja .. -DBUILD_UNIT_TESTS=OFF -DBUILD_EXAMPLES=OFF -DUSE_HTTP=ON -DENABLE_SCCACHE=ON -DUSE_CXL=ON -DWITH_EP=ON -DEP_TORCH_VERSIONS="2.9.0;2.9.1;2.10.0"
|
||||
shell: bash
|
||||
|
||||
- name: Build project
|
||||
|
|
@ -438,8 +430,8 @@ jobs:
|
|||
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
|
||||
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
|
||||
cd build
|
||||
make -j4
|
||||
sudo make install
|
||||
cmake --build .
|
||||
sudo cmake --install .
|
||||
shell: bash
|
||||
|
||||
- name: Build nvlink_allocator.so
|
||||
|
|
@ -503,12 +495,16 @@ jobs:
|
|||
name: Check code format
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.action == 'opened' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Need full history for branch comparison
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install clang-format 20
|
||||
run: |
|
||||
|
|
@ -517,17 +513,31 @@ jobs:
|
|||
sudo ./llvm.sh 20
|
||||
sudo apt-get install -y clang-format-20
|
||||
|
||||
- name: run clang-format-20
|
||||
- name: Check code format
|
||||
run: |
|
||||
# the old clang-format-14 which is the default version in ubuntu 22.04,
|
||||
# is inconsistent with clang-format-20.
|
||||
ls -lh /usr/bin/clang-format*
|
||||
clang-format --version
|
||||
clang-format-20 --version
|
||||
# skip cachelib_memory_allocator
|
||||
find . -type f \( -name "*.h" -o -name "*.cpp" \) | grep -v cachelib_memory_allocator | xargs clang-format-20 -style=file -i
|
||||
if ! git diff --exit-code; then
|
||||
echo "Please follow the .clang-format code style, try clang-format -i FILENAME"
|
||||
# Check script exists and is executable
|
||||
if [[ ! -x ./scripts/code_format.sh ]]; then
|
||||
echo "Error: code_format.sh not found or not executable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine base ref for comparison
|
||||
if [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||
# For PRs: compare against the target branch
|
||||
BASE_REF="origin/${{ github.base_ref }}"
|
||||
else
|
||||
# For push events: use github.event.before to handle multi-commit pushes
|
||||
BEFORE_SHA="${{ github.event.before }}"
|
||||
if [ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" ]; then
|
||||
# New branch push, compare against default branch
|
||||
BASE_REF="origin/${{ github.event.repository.default_branch }}"
|
||||
else
|
||||
# Normal push (single or multiple commits)
|
||||
BASE_REF="${BEFORE_SHA}"
|
||||
fi
|
||||
fi
|
||||
echo "Comparing against: ${BASE_REF}"
|
||||
./scripts/code_format.sh --check --base "${BASE_REF}"
|
||||
shell: bash
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
name: 'Build Wheel (CUDA 13)'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
jobs:
|
||||
build-wheel-cu13:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
github.event.action == 'opened' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-ci')
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.12']
|
||||
env:
|
||||
BUILD_WITH_EP: "1"
|
||||
EP_TORCH_VERSIONS: "2.9.0;2.9.1;2.10.0"
|
||||
TORCH_CUDA_ARCH_LIST: "8.0;9.0"
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
df -h
|
||||
|
||||
- name: Install CUDA Toolkit
|
||||
uses: Jimver/cuda-toolkit@v0.2.29
|
||||
with:
|
||||
cuda: '13.0.2'
|
||||
linux-local-args: '["--toolkit"]'
|
||||
method: 'network'
|
||||
sub-packages: '["nvcc", "nvrtc-dev"]'
|
||||
non-cuda-sub-packages: '["libcusparse-dev", "libcublas-dev", "libcusolver-dev"]'
|
||||
|
||||
- name: Run sccache-cache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: Configure sccache
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Run sccache stat for check
|
||||
shell: bash
|
||||
run: ${SCCACHE_PATH} --show-stats
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update -y
|
||||
sudo apt install -y ninja-build
|
||||
sudo bash -x dependencies.sh -y
|
||||
df -h
|
||||
shell: bash
|
||||
|
||||
- name: Configure project
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -G Ninja .. \
|
||||
-DUSE_ETCD=ON \
|
||||
-DUSE_REDIS=ON \
|
||||
-DUSE_HTTP=ON \
|
||||
-DWITH_STORE=ON \
|
||||
-DWITH_P2P_STORE=ON \
|
||||
-DWITH_EP=ON \
|
||||
-DWITH_METRICS=ON \
|
||||
-DBUILD_UNIT_TESTS=OFF \
|
||||
-DBUILD_EXAMPLES=ON \
|
||||
-DENABLE_SCCACHE=ON \
|
||||
-DBUILD_BENCHMARK=ON \
|
||||
-DUSE_CUDA=ON \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
|
||||
shell: bash
|
||||
|
||||
- name: Build project
|
||||
run: |
|
||||
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
|
||||
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
|
||||
cd build
|
||||
cmake --build .
|
||||
sudo cmake --install .
|
||||
df -h
|
||||
shell: bash
|
||||
|
||||
- name: Build nvlink_allocator.so
|
||||
run: |
|
||||
mkdir -p build/mooncake-transfer-engine/nvlink-allocator
|
||||
cd mooncake-transfer-engine/nvlink-allocator
|
||||
export PATH=/usr/local/nvidia/bin:/usr/local/nvidia/lib64:$PATH
|
||||
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
|
||||
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
|
||||
bash build.sh ../../build/mooncake-transfer-engine/nvlink-allocator/
|
||||
shell: bash
|
||||
|
||||
- name: Generate Python version tag
|
||||
id: generate_tag
|
||||
run: |
|
||||
echo "python_version_tag=$(echo ${{ matrix.python-version }} | tr -d '.')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Build Python wheel
|
||||
run: |
|
||||
PYTHON_VERSION=${{ matrix.python-version }} OUTPUT_DIR=dist-py${{ steps.generate_tag.outputs.python_version_tag }} ./scripts/build_wheel.sh
|
||||
shell: bash
|
||||
|
||||
- name: Upload Python wheel artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mooncake-wheel-cu130-ubuntu-py${{ steps.generate_tag.outputs.python_version_tag }}
|
||||
path: mooncake-wheel/dist-py${{ steps.generate_tag.outputs.python_version_tag }}/*.whl
|
||||
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
|
||||
env:
|
||||
BUILD_WITH_EP: "1"
|
||||
EP_TORCH_VERSIONS: "2.8.0;2.9.0;2.9.1"
|
||||
EP_TORCH_VERSIONS: "2.9.0;2.9.1;2.10.0"
|
||||
TORCH_CUDA_ARCH_LIST: "8.0;9.0"
|
||||
steps:
|
||||
- name: Checkout source
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ repos:
|
|||
args: ['--ignore-words-list=te,mooncake,KVCache']
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v19.1.0
|
||||
rev: v20.1.8
|
||||
hooks:
|
||||
- id: clang-format
|
||||
files: '\.(c|cc|cpp|cxx|h|hpp)$'
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ if (STORE_USE_ETCD)
|
|||
endif()
|
||||
|
||||
option(STORE_USE_JEMALLOC "Use jemalloc in mooncake store master" OFF)
|
||||
option(USE_ASCEND_CACHE_TIER "Enable Ascend NPU cache tier support" OFF)
|
||||
|
||||
add_subdirectory(mooncake-common)
|
||||
include_directories(mooncake-common/etcd)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ echo -e "${YELLOW}This may take a few minutes...${NC}"
|
|||
|
||||
SYSTEM_PACKAGES="build-essential \
|
||||
cmake \
|
||||
ninja-build \
|
||||
git \
|
||||
wget \
|
||||
libibverbs-dev \
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ add_definitions(-DCONFIG_ERDMA)
|
|||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Memory-aware build parallelism (compile vs. link job pools)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/limit_jobs.cmake)
|
||||
|
||||
option(ENABLE_SCCACHE "Whether to open sccache" OFF)
|
||||
if (ENABLE_SCCACHE)
|
||||
find_program(SCCACHE sccache REQUIRED)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
# limit_jobs.cmake — Memory-aware build parallelism
|
||||
#
|
||||
# Auto-detects available memory and CPU count, calculates safe parallel job
|
||||
# limits for compilation and linking separately. With Ninja, creates job pools
|
||||
# so compilation uses many cores while memory-heavy linking is restricted.
|
||||
#
|
||||
# User overrides (cmake -D...):
|
||||
# PARALLEL_COMPILE_JOBS — override compile parallelism
|
||||
# PARALLEL_LINK_JOBS — override link parallelism
|
||||
# MAX_COMPILER_MEMORY_MB — per-compile-job memory estimate (default: 1500)
|
||||
# MAX_LINKER_MEMORY_MB — per-link-job memory estimate (default: 4000)
|
||||
|
||||
set(MAX_COMPILER_MEMORY_MB "1500" CACHE STRING
|
||||
"Estimated peak memory per compile job in MB")
|
||||
set(MAX_LINKER_MEMORY_MB "4000" CACHE STRING
|
||||
"Estimated peak memory per link job in MB")
|
||||
|
||||
# Guard against invalid user input (division by zero)
|
||||
if(MAX_COMPILER_MEMORY_MB LESS_EQUAL 0)
|
||||
message(WARNING "[limit_jobs] MAX_COMPILER_MEMORY_MB=${MAX_COMPILER_MEMORY_MB} "
|
||||
"invalid, falling back to 1500")
|
||||
set(MAX_COMPILER_MEMORY_MB 1500 CACHE STRING
|
||||
"Estimated peak memory per compile job in MB" FORCE)
|
||||
endif()
|
||||
if(MAX_LINKER_MEMORY_MB LESS_EQUAL 0)
|
||||
message(WARNING "[limit_jobs] MAX_LINKER_MEMORY_MB=${MAX_LINKER_MEMORY_MB} "
|
||||
"invalid, falling back to 4000")
|
||||
set(MAX_LINKER_MEMORY_MB 4000 CACHE STRING
|
||||
"Estimated peak memory per link job in MB" FORCE)
|
||||
endif()
|
||||
|
||||
# Detect system resources
|
||||
cmake_host_system_information(RESULT _available_mem_mb
|
||||
QUERY AVAILABLE_PHYSICAL_MEMORY)
|
||||
cmake_host_system_information(RESULT _nproc
|
||||
QUERY NUMBER_OF_LOGICAL_CORES)
|
||||
|
||||
message(STATUS "[limit_jobs] Available memory: ${_available_mem_mb} MB, "
|
||||
"CPU cores: ${_nproc}")
|
||||
|
||||
# Calculate safe parallel jobs from memory
|
||||
math(EXPR _compile_jobs "${_available_mem_mb} / ${MAX_COMPILER_MEMORY_MB}")
|
||||
math(EXPR _link_jobs "${_available_mem_mb} / ${MAX_LINKER_MEMORY_MB}")
|
||||
|
||||
# Clamp: [1, nproc]
|
||||
if(_compile_jobs LESS 1)
|
||||
set(_compile_jobs 1)
|
||||
endif()
|
||||
if(_compile_jobs GREATER _nproc)
|
||||
set(_compile_jobs ${_nproc})
|
||||
endif()
|
||||
if(_link_jobs LESS 1)
|
||||
set(_link_jobs 1)
|
||||
endif()
|
||||
if(_link_jobs GREATER _nproc)
|
||||
set(_link_jobs ${_nproc})
|
||||
endif()
|
||||
|
||||
# Use auto-detected values unless user explicitly overrides with -D
|
||||
if(NOT DEFINED PARALLEL_COMPILE_JOBS)
|
||||
set(PARALLEL_COMPILE_JOBS "${_compile_jobs}")
|
||||
endif()
|
||||
if(NOT DEFINED PARALLEL_LINK_JOBS)
|
||||
set(PARALLEL_LINK_JOBS "${_link_jobs}")
|
||||
endif()
|
||||
|
||||
message(STATUS "[limit_jobs] Compile jobs: ${PARALLEL_COMPILE_JOBS} "
|
||||
"(${MAX_COMPILER_MEMORY_MB} MB/job), "
|
||||
"Link jobs: ${PARALLEL_LINK_JOBS} (${MAX_LINKER_MEMORY_MB} MB/job)")
|
||||
|
||||
# Apply to build system
|
||||
if(CMAKE_GENERATOR MATCHES "Ninja")
|
||||
set_property(GLOBAL APPEND PROPERTY JOB_POOLS
|
||||
compile_pool=${PARALLEL_COMPILE_JOBS}
|
||||
link_pool=${PARALLEL_LINK_JOBS}
|
||||
)
|
||||
set(CMAKE_JOB_POOL_COMPILE "compile_pool" CACHE STRING "" FORCE)
|
||||
set(CMAKE_JOB_POOL_LINK "link_pool" CACHE STRING "" FORCE)
|
||||
message(STATUS "[limit_jobs] Ninja job pools: "
|
||||
"compile=${PARALLEL_COMPILE_JOBS}, link=${PARALLEL_LINK_JOBS}")
|
||||
else()
|
||||
message(STATUS "[limit_jobs] Hint: use -G Ninja for automatic "
|
||||
"compile/link parallelism separation")
|
||||
message(STATUS "[limit_jobs] With Make, recommend: "
|
||||
"cmake --build . -j${PARALLEL_LINK_JOBS}")
|
||||
endif()
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -20,11 +20,12 @@
|
|||
#include <random>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <future>
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
#include "glog/logging.h"
|
||||
|
||||
#include "master_client.h"
|
||||
#include "centralized_master_client.h"
|
||||
|
||||
// Size units for better readability
|
||||
static constexpr size_t KiB = 1024;
|
||||
|
|
@ -56,7 +57,7 @@ class SegmentClient {
|
|||
public:
|
||||
SegmentClient(const std::string& name, const std::string& master_server,
|
||||
uintptr_t segment_base, uint64_t segment_size)
|
||||
: master_client_(mooncake::generate_uuid()) {
|
||||
: client_id_(mooncake::generate_uuid()), master_client_(client_id_) {
|
||||
auto ec = master_client_.Connect(master_server);
|
||||
if (ec != mooncake::ErrorCode::OK) {
|
||||
throw std::invalid_argument("Cannot connect to master server at " +
|
||||
|
|
@ -65,9 +66,9 @@ class SegmentClient {
|
|||
|
||||
segment_.id = mooncake::generate_uuid();
|
||||
segment_.name = name;
|
||||
segment_.base = segment_base;
|
||||
segment_.size = segment_size;
|
||||
segment_.te_endpoint = name;
|
||||
segment_.extra = mooncake::CentralizedSegmentExtraData{
|
||||
.base = segment_base, .te_endpoint = name};
|
||||
auto mount_ec = master_client_.MountSegment(segment_);
|
||||
if (!mount_ec.has_value()) {
|
||||
throw std::runtime_error("Failed to mount segment " + name +
|
||||
|
|
@ -86,7 +87,7 @@ class SegmentClient {
|
|||
}
|
||||
}
|
||||
|
||||
void Ping() {
|
||||
void Heartbeat() {
|
||||
if (remount_future_.valid() &&
|
||||
remount_future_.wait_for(std::chrono::seconds(0)) ==
|
||||
std::future_status::ready) {
|
||||
|
|
@ -94,25 +95,31 @@ class SegmentClient {
|
|||
remount_future_ = std::future<void>();
|
||||
}
|
||||
|
||||
auto ping_result = master_client_.Ping();
|
||||
if (!ping_result.has_value()) {
|
||||
throw std::runtime_error("Failed to ping master server");
|
||||
mooncake::HeartbeatRequest req;
|
||||
req.client_id = client_id_;
|
||||
auto heartbeat_result = master_client_.Heartbeat(req);
|
||||
if (!heartbeat_result.has_value()) {
|
||||
throw std::runtime_error("Failed to heartbeat to master server");
|
||||
}
|
||||
|
||||
if (ping_result.value().client_status ==
|
||||
mooncake::ClientStatus::NEED_REMOUNT &&
|
||||
if (heartbeat_result.value().status ==
|
||||
mooncake::ClientStatus::UNDEFINED &&
|
||||
!remount_future_.valid()) {
|
||||
remount_future_ = std::async(std::launch::async, [&]() {
|
||||
auto remount_ec = master_client_.ReMountSegment({segment_});
|
||||
if (!remount_ec.has_value()) {
|
||||
throw std::runtime_error("Failed to remount segment");
|
||||
mooncake::RegisterClientRequest req;
|
||||
req.client_id = client_id_;
|
||||
req.segments = {segment_};
|
||||
auto reg_ec = master_client_.RegisterClient(req);
|
||||
if (!reg_ec.has_value()) {
|
||||
throw std::runtime_error("Failed to register client");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
mooncake::MasterClient master_client_;
|
||||
mooncake::UUID client_id_;
|
||||
mooncake::CentralizedMasterClient master_client_;
|
||||
mooncake::Segment segment_;
|
||||
std::future<void> remount_future_;
|
||||
};
|
||||
|
|
@ -349,7 +356,7 @@ class BenchClient {
|
|||
}
|
||||
}
|
||||
|
||||
mooncake::MasterClient master_client_;
|
||||
mooncake::CentralizedMasterClient master_client_;
|
||||
|
||||
std::atomic<bool> running_;
|
||||
std::vector<std::thread> threads_;
|
||||
|
|
@ -377,7 +384,7 @@ int main(int argc, char** argv) {
|
|||
{
|
||||
std::lock_guard<std::mutex> guard(segment_clients_mutex);
|
||||
for (auto& segment_client : segment_clients) {
|
||||
segment_client->Ping();
|
||||
segment_client->Heartbeat();
|
||||
}
|
||||
}
|
||||
time_elapsed = std::chrono::steady_clock::now() - start_time;
|
||||
|
|
@ -437,12 +444,12 @@ int main(int argc, char** argv) {
|
|||
}
|
||||
LOG(INFO) << "Clients stopped";
|
||||
|
||||
LOG(INFO) << "Stopping ping thread...";
|
||||
LOG(INFO) << "Stopping heartbeat thread...";
|
||||
if (ping_thread.joinable()) {
|
||||
ping_thread.request_stop();
|
||||
ping_thread.join();
|
||||
}
|
||||
LOG(INFO) << "Ping thread stopped";
|
||||
LOG(INFO) << "Heartbeat thread stopped";
|
||||
|
||||
LOG(INFO) << "Disconnecting from master...";
|
||||
bench_clients.clear();
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ class AllocatedBuffer {
|
|||
struct Descriptor;
|
||||
|
||||
AllocatedBuffer(std::shared_ptr<BufferAllocatorBase> allocator,
|
||||
void* buffer_ptr, std::size_t size,
|
||||
void* buffer_ptr, std::size_t size, const UUID& segment_id,
|
||||
std::optional<offset_allocator::OffsetAllocationHandle>&&
|
||||
offset_handle = std::nullopt)
|
||||
: allocator_(std::move(allocator)),
|
||||
buffer_ptr_(buffer_ptr),
|
||||
size_(size),
|
||||
segment_id_(segment_id),
|
||||
offset_handle_(std::move(offset_handle)) {}
|
||||
|
||||
~AllocatedBuffer();
|
||||
|
|
@ -58,6 +59,8 @@ class AllocatedBuffer {
|
|||
|
||||
[[nodiscard]] std::string getSegmentName() const noexcept;
|
||||
|
||||
[[nodiscard]] UUID getSegmentId() const noexcept { return segment_id_; }
|
||||
|
||||
// Friend declaration for operator<<
|
||||
friend std::ostream& operator<<(std::ostream& os,
|
||||
const AllocatedBuffer& buffer);
|
||||
|
|
@ -74,6 +77,7 @@ class AllocatedBuffer {
|
|||
std::weak_ptr<BufferAllocatorBase> allocator_;
|
||||
void* buffer_ptr_{nullptr};
|
||||
std::size_t size_{0};
|
||||
UUID segment_id_;
|
||||
// RAII handle for buffer allocated by offset allocator
|
||||
std::optional<offset_allocator::OffsetAllocationHandle> offset_handle_{
|
||||
std::nullopt};
|
||||
|
|
@ -92,6 +96,7 @@ class BufferAllocatorBase {
|
|||
virtual size_t capacity() const = 0;
|
||||
virtual size_t size() const = 0;
|
||||
virtual std::string getSegmentName() const = 0;
|
||||
virtual UUID getSegmentId() const = 0;
|
||||
virtual std::string getTransportEndpoint() const = 0;
|
||||
|
||||
/**
|
||||
|
|
@ -133,7 +138,8 @@ class CachelibBufferAllocator
|
|||
public std::enable_shared_from_this<CachelibBufferAllocator> {
|
||||
public:
|
||||
CachelibBufferAllocator(std::string segment_name, size_t base, size_t size,
|
||||
std::string transport_endpoint);
|
||||
std::string transport_endpoint,
|
||||
const UUID& segment_id);
|
||||
|
||||
~CachelibBufferAllocator() override;
|
||||
|
||||
|
|
@ -144,6 +150,7 @@ class CachelibBufferAllocator
|
|||
size_t capacity() const override { return total_size_; }
|
||||
size_t size() const override { return cur_size_.load(); }
|
||||
std::string getSegmentName() const override { return segment_name_; }
|
||||
UUID getSegmentId() const override { return segment_id_; }
|
||||
std::string getTransportEndpoint() const override {
|
||||
return transport_endpoint_;
|
||||
}
|
||||
|
|
@ -164,6 +171,7 @@ class CachelibBufferAllocator
|
|||
const size_t total_size_;
|
||||
std::atomic_size_t cur_size_;
|
||||
const std::string transport_endpoint_;
|
||||
const UUID segment_id_;
|
||||
|
||||
// metrics - removed allocated_bytes_ member
|
||||
// ylt::metric::gauge_t* allocated_bytes_{nullptr};
|
||||
|
|
@ -184,7 +192,8 @@ class OffsetBufferAllocator
|
|||
public std::enable_shared_from_this<OffsetBufferAllocator> {
|
||||
public:
|
||||
OffsetBufferAllocator(std::string segment_name, size_t base, size_t size,
|
||||
std::string transport_endpoint);
|
||||
std::string transport_endpoint,
|
||||
const UUID& segment_id);
|
||||
|
||||
~OffsetBufferAllocator() override;
|
||||
|
||||
|
|
@ -195,6 +204,7 @@ class OffsetBufferAllocator
|
|||
size_t capacity() const override { return total_size_; }
|
||||
size_t size() const override { return cur_size_.load(); }
|
||||
std::string getSegmentName() const override { return segment_name_; }
|
||||
UUID getSegmentId() const override { return segment_id_; }
|
||||
std::string getTransportEndpoint() const override {
|
||||
return transport_endpoint_;
|
||||
}
|
||||
|
|
@ -211,6 +221,7 @@ class OffsetBufferAllocator
|
|||
const size_t total_size_;
|
||||
std::atomic_size_t cur_size_;
|
||||
const std::string transport_endpoint_;
|
||||
const UUID segment_id_;
|
||||
|
||||
// offset allocator implementation
|
||||
std::shared_ptr<offset_allocator::OffsetAllocator> offset_allocator_;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "tiered_cache/tiered_backend.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
// ============================================================================
|
||||
// LocalCopyPlan — describes a local memcpy operation
|
||||
// ============================================================================
|
||||
|
||||
struct LocalCopyPlan {
|
||||
AllocationHandle source_handle;
|
||||
const char* source_ptr = nullptr;
|
||||
size_t source_size = 0;
|
||||
bool use_single_dest = false;
|
||||
void* single_dest_ptr = nullptr;
|
||||
size_t single_dest_size = 0;
|
||||
std::vector<Slice> dest_slices;
|
||||
};
|
||||
|
||||
ErrorCode ExecuteLocalCopyPlan(const LocalCopyPlan& plan);
|
||||
|
||||
// ============================================================================
|
||||
// AsyncMemcpyExecutor
|
||||
// ============================================================================
|
||||
|
||||
class AsyncMemcpyExecutor {
|
||||
public:
|
||||
template <typename ResultType>
|
||||
struct BatchState {
|
||||
std::vector<ResultType> results;
|
||||
std::atomic<size_t> remaining{0};
|
||||
std::mutex done_mutex;
|
||||
std::condition_variable done_cv;
|
||||
bool done = false;
|
||||
};
|
||||
|
||||
template <typename ResultType>
|
||||
struct BatchHandle {
|
||||
std::shared_ptr<BatchState<ResultType>> state;
|
||||
|
||||
std::vector<ResultType> Wait() const {
|
||||
if (!state) {
|
||||
return {};
|
||||
}
|
||||
if (state->remaining.load(std::memory_order_acquire) > 0) {
|
||||
std::unique_lock<std::mutex> lock(state->done_mutex);
|
||||
auto state_ptr = state;
|
||||
state->done_cv.wait(lock,
|
||||
[state_ptr] { return state_ptr->done; });
|
||||
}
|
||||
return state->results;
|
||||
}
|
||||
};
|
||||
|
||||
explicit AsyncMemcpyExecutor(size_t worker_num);
|
||||
~AsyncMemcpyExecutor();
|
||||
|
||||
template <typename ResultType, typename TaskFn, typename ErrorFn>
|
||||
BatchHandle<ResultType> SubmitBatchTasks(const std::vector<size_t>& indices,
|
||||
TaskFn&& task_fn,
|
||||
ErrorFn&& on_error);
|
||||
|
||||
template <typename ResultType, typename Fn>
|
||||
std::future<ResultType> SubmitSingleTask(Fn&& fn);
|
||||
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
struct QueueTask {
|
||||
std::function<void()> run;
|
||||
std::function<void()> cancel;
|
||||
};
|
||||
|
||||
void WorkerMain();
|
||||
|
||||
template <typename ResultType>
|
||||
static void FinishBatchTask(
|
||||
const std::shared_ptr<BatchState<ResultType>>& state,
|
||||
size_t batch_index, ResultType result) {
|
||||
if (!state || batch_index >= state->results.size()) {
|
||||
return;
|
||||
}
|
||||
state->results[batch_index] = std::move(result);
|
||||
if (state->remaining.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||
std::lock_guard<std::mutex> lock(state->done_mutex);
|
||||
state->done = true;
|
||||
state->done_cv.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
bool shutting_down_ = false;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable queue_not_empty_cv_;
|
||||
std::queue<QueueTask> tasks_;
|
||||
std::vector<std::thread> workers_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Template method implementations (must be in header)
|
||||
// ============================================================================
|
||||
|
||||
template <typename ResultType, typename TaskFn, typename ErrorFn>
|
||||
AsyncMemcpyExecutor::BatchHandle<ResultType>
|
||||
AsyncMemcpyExecutor::SubmitBatchTasks(const std::vector<size_t>& indices,
|
||||
TaskFn&& task_fn, ErrorFn&& on_error) {
|
||||
auto batch_state = std::make_shared<BatchState<ResultType>>();
|
||||
batch_state->results.reserve(indices.size());
|
||||
|
||||
using TaskFnType = typename std::decay<TaskFn>::type;
|
||||
using ErrorFnType = typename std::decay<ErrorFn>::type;
|
||||
auto task_fn_ptr =
|
||||
std::make_shared<TaskFnType>(std::forward<TaskFn>(task_fn));
|
||||
auto on_error_ptr =
|
||||
std::make_shared<ErrorFnType>(std::forward<ErrorFn>(on_error));
|
||||
|
||||
for (size_t index : indices) {
|
||||
batch_state->results.push_back((*on_error_ptr)(index));
|
||||
}
|
||||
batch_state->remaining.store(indices.size(), std::memory_order_relaxed);
|
||||
if (indices.empty()) {
|
||||
batch_state->done = true;
|
||||
return BatchHandle<ResultType>{batch_state};
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (size_t enqueued = 0; enqueued < indices.size(); ++enqueued) {
|
||||
if (shutting_down_) {
|
||||
// Cancel remaining slots synchronously.
|
||||
for (size_t slot = enqueued; slot < indices.size(); ++slot) {
|
||||
FinishBatchTask(batch_state, slot,
|
||||
(*on_error_ptr)(indices[slot]));
|
||||
}
|
||||
return BatchHandle<ResultType>{batch_state};
|
||||
}
|
||||
|
||||
const size_t slot = enqueued;
|
||||
const size_t index = indices[slot];
|
||||
QueueTask queue_task;
|
||||
queue_task.run = [batch_state, task_fn_ptr, on_error_ptr, slot,
|
||||
index]() mutable {
|
||||
ResultType result = (*on_error_ptr)(index);
|
||||
try {
|
||||
result = (*task_fn_ptr)(index);
|
||||
} catch (const std::exception& e) {
|
||||
LOG(ERROR) << "Async batch task threw at index " << index
|
||||
<< ": " << e.what();
|
||||
result = (*on_error_ptr)(index);
|
||||
} catch (...) {
|
||||
LOG(ERROR) << "Async batch task threw unknown exception "
|
||||
"at index "
|
||||
<< index;
|
||||
result = (*on_error_ptr)(index);
|
||||
}
|
||||
FinishBatchTask(batch_state, slot, std::move(result));
|
||||
};
|
||||
queue_task.cancel = [batch_state, on_error_ptr, slot, index]() {
|
||||
FinishBatchTask(batch_state, slot, (*on_error_ptr)(index));
|
||||
};
|
||||
tasks_.push(std::move(queue_task));
|
||||
}
|
||||
}
|
||||
queue_not_empty_cv_.notify_all();
|
||||
return BatchHandle<ResultType>{batch_state};
|
||||
}
|
||||
|
||||
template <typename ResultType, typename Fn>
|
||||
std::future<ResultType> AsyncMemcpyExecutor::SubmitSingleTask(Fn&& fn) {
|
||||
auto promise = std::make_shared<std::promise<ResultType>>();
|
||||
auto future = promise->get_future();
|
||||
|
||||
using FnType = typename std::decay<Fn>::type;
|
||||
auto fn_ptr = std::make_shared<FnType>(std::forward<Fn>(fn));
|
||||
|
||||
QueueTask task;
|
||||
task.run = [promise, fn_ptr]() {
|
||||
try {
|
||||
promise->set_value((*fn_ptr)());
|
||||
} catch (...) {
|
||||
promise->set_exception(std::current_exception());
|
||||
}
|
||||
};
|
||||
task.cancel = [promise]() {
|
||||
try {
|
||||
promise->set_exception(
|
||||
std::make_exception_ptr(std::runtime_error("task cancelled")));
|
||||
} catch (...) {
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (shutting_down_) {
|
||||
task.cancel();
|
||||
return future;
|
||||
}
|
||||
tasks_.push(std::move(task));
|
||||
}
|
||||
queue_not_empty_cv_.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
|
||||
#include "p2p_master_client.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
// Called when an async ADD fails with a non-transient error.
|
||||
// Caller should delete the local replica.
|
||||
using SyncFailureCallback = std::function<void(
|
||||
const std::string& key, const UUID& segment_id, ErrorCode error)>;
|
||||
|
||||
class AsyncMetadataNotifier {
|
||||
public:
|
||||
AsyncMetadataNotifier(P2PMasterClient& master_client, const UUID& client_id,
|
||||
size_t sender_thread_count, size_t max_batch_size,
|
||||
size_t queue_capacity,
|
||||
SyncFailureCallback failure_cb = nullptr);
|
||||
~AsyncMetadataNotifier();
|
||||
|
||||
AsyncMetadataNotifier(const AsyncMetadataNotifier&) = delete;
|
||||
AsyncMetadataNotifier& operator=(const AsyncMetadataNotifier&) = delete;
|
||||
|
||||
// Start/Stop can be called alternately.
|
||||
// drop_pending=false (default): sender drains remaining ops before exiting.
|
||||
// drop_pending=true: queued ops are discarded immediately without invoking
|
||||
// failure_cb.
|
||||
void Start();
|
||||
void Stop(bool drop_pending = false);
|
||||
|
||||
// --- Normal priority ---
|
||||
tl::expected<void, ErrorCode> EnqueueAdd(const std::string& key,
|
||||
const UUID& segment_id,
|
||||
size_t size);
|
||||
|
||||
tl::expected<void, ErrorCode> EnqueueRemove(const std::string& key,
|
||||
const UUID& segment_id);
|
||||
|
||||
// --- Recovery priority (lower send priority) ---
|
||||
tl::expected<void, ErrorCode> EnqueueRecoveryAdd(const std::string& key,
|
||||
const UUID& segment_id,
|
||||
size_t size);
|
||||
|
||||
/**
|
||||
* @brief Wait until all recovery ops have been sent.
|
||||
* Only valid after the caller has finished enqueueing all recovery ops.
|
||||
* @return true if drained, false if aborted or timed out.
|
||||
*/
|
||||
bool WaitForRecoveryDrain(const std::function<bool()>& abort_fn,
|
||||
std::chrono::milliseconds timeout);
|
||||
|
||||
bool IsPaused() const {
|
||||
return consecutive_rpc_failures_.load(std::memory_order_acquire) < 0;
|
||||
}
|
||||
|
||||
private:
|
||||
struct PendingOp {
|
||||
enum Type : int32_t { ADD = 0, REMOVE = 1 };
|
||||
|
||||
Type type = ADD;
|
||||
std::string key;
|
||||
UUID segment_id{};
|
||||
size_t size = 0;
|
||||
};
|
||||
|
||||
// Identifier of a Key. We assume that each key is unique in a segment.
|
||||
struct CoalesceKey {
|
||||
std::string key;
|
||||
UUID segment_id;
|
||||
|
||||
bool operator==(const CoalesceKey& o) const {
|
||||
return key == o.key && segment_id == o.segment_id;
|
||||
}
|
||||
};
|
||||
|
||||
struct CoalesceKeyHash {
|
||||
size_t operator()(const CoalesceKey& k) const {
|
||||
size_t h = std::hash<std::string>{}(k.key);
|
||||
boost::hash_combine(h, k.segment_id);
|
||||
return h;
|
||||
}
|
||||
};
|
||||
|
||||
// Coalescing entry: tracks slot index of a pending op for a key.
|
||||
// InvalidIdx means no pending op of that type.
|
||||
static constexpr size_t InvalidIdx = SIZE_MAX;
|
||||
struct CoalesceEntry {
|
||||
size_t add_idx = InvalidIdx;
|
||||
size_t remove_idx = InvalidIdx;
|
||||
};
|
||||
|
||||
// Pool node: PendingOp data + intrusive doubly-linked list pointers.
|
||||
struct Slot {
|
||||
PendingOp op;
|
||||
size_t prev = InvalidIdx;
|
||||
size_t next = InvalidIdx;
|
||||
bool is_recovery = false; // which list this slot belongs to
|
||||
};
|
||||
|
||||
struct SenderShard {
|
||||
public:
|
||||
SenderShard() = default;
|
||||
SenderShard(const SenderShard&) = delete;
|
||||
SenderShard& operator=(const SenderShard&) = delete;
|
||||
SenderShard(SenderShard&&) = delete;
|
||||
SenderShard& operator=(SenderShard&&) = delete;
|
||||
|
||||
bool IsFull() const { return free_stack.empty(); }
|
||||
bool IsFullForRecovery() const {
|
||||
return free_stack.size() <= normal_reserved;
|
||||
}
|
||||
bool IsEmpty() const {
|
||||
return normal_count == 0 && recovery_count == 0;
|
||||
}
|
||||
|
||||
size_t AllocSlot() {
|
||||
size_t idx = free_stack.back();
|
||||
free_stack.pop_back();
|
||||
return idx;
|
||||
}
|
||||
|
||||
void FreeSlot(size_t idx) {
|
||||
slots[idx].prev = InvalidIdx;
|
||||
slots[idx].next = InvalidIdx;
|
||||
slots[idx].is_recovery = false;
|
||||
free_stack.push_back(idx);
|
||||
}
|
||||
|
||||
// Link to the tail of the appropriate list based on is_recovery flag.
|
||||
void LinkTail(size_t idx, bool is_recovery) {
|
||||
auto& head = is_recovery ? recovery_head : normal_head;
|
||||
auto& tail = is_recovery ? recovery_tail : normal_tail;
|
||||
auto& count = is_recovery ? recovery_count : normal_count;
|
||||
|
||||
slots[idx].prev = tail;
|
||||
slots[idx].next = InvalidIdx;
|
||||
slots[idx].is_recovery = is_recovery;
|
||||
if (tail != InvalidIdx) {
|
||||
slots[tail].next = idx;
|
||||
} else {
|
||||
head = idx;
|
||||
}
|
||||
tail = idx;
|
||||
count++;
|
||||
}
|
||||
|
||||
// Unlink from whichever list the slot belongs to.
|
||||
void Unlink(size_t idx) {
|
||||
auto& slot = slots[idx];
|
||||
auto& head = slot.is_recovery ? recovery_head : normal_head;
|
||||
auto& tail = slot.is_recovery ? recovery_tail : normal_tail;
|
||||
auto& count = slot.is_recovery ? recovery_count : normal_count;
|
||||
|
||||
if (slot.prev != InvalidIdx) {
|
||||
slots[slot.prev].next = slot.next;
|
||||
} else {
|
||||
head = slot.next;
|
||||
}
|
||||
if (slot.next != InvalidIdx) {
|
||||
slots[slot.next].prev = slot.prev;
|
||||
} else {
|
||||
tail = slot.prev;
|
||||
}
|
||||
count--;
|
||||
}
|
||||
|
||||
public:
|
||||
std::mutex mutex;
|
||||
std::condition_variable sender_cv; // sender waits when both empty
|
||||
std::condition_variable producer_cv; // normal producer waits when full
|
||||
std::condition_variable recovery_drain_cv; // WaitForRecoveryDrain
|
||||
|
||||
// Pre-allocated slot pool — shared by both queues
|
||||
std::vector<Slot> slots;
|
||||
std::vector<size_t> free_stack;
|
||||
size_t capacity = 0;
|
||||
size_t normal_reserved = 0; // slots reserved for normal ops
|
||||
|
||||
// Normal priority linked list
|
||||
size_t normal_head = InvalidIdx;
|
||||
size_t normal_tail = InvalidIdx;
|
||||
size_t normal_count = 0;
|
||||
|
||||
// Recovery (low priority) linked list
|
||||
size_t recovery_head = InvalidIdx;
|
||||
size_t recovery_tail = InvalidIdx;
|
||||
size_t recovery_count = 0;
|
||||
|
||||
// Shared coalesce index (covers both queues)
|
||||
std::unordered_map<CoalesceKey, CoalesceEntry, CoalesceKeyHash>
|
||||
coalesce_index;
|
||||
|
||||
// Recovery ops that have been dequeued by CollectBatch but whose
|
||||
// SendBatch has not yet completed. WaitForRecoveryDrain waits for
|
||||
// both recovery_count and recovery_in_flight to reach zero.
|
||||
size_t recovery_in_flight = 0;
|
||||
|
||||
std::thread sender_thread;
|
||||
};
|
||||
|
||||
private:
|
||||
tl::expected<void, ErrorCode> DoEnqueue(PendingOp&& op, bool is_recovery);
|
||||
|
||||
void SenderLoop(size_t shard_idx);
|
||||
// Returns (total_collected, recovery_collected)
|
||||
std::pair<size_t, size_t> CollectBatch(SenderShard& shard,
|
||||
std::vector<PendingOp>& batch_out);
|
||||
size_t CollectFromList(SenderShard& shard,
|
||||
std::vector<PendingOp>& batch_out, size_t offset,
|
||||
size_t max_count, bool from_recovery);
|
||||
void SendBatch(std::vector<PendingOp>& batch, size_t count);
|
||||
void RecordSuccess();
|
||||
void RecordFailure();
|
||||
void ResetShard(SenderShard& shard);
|
||||
|
||||
private:
|
||||
static constexpr int MaxRetryCount = 3;
|
||||
static constexpr int CircuitBreakerThreshold = 5;
|
||||
static constexpr auto BatchTimeout = std::chrono::milliseconds(20);
|
||||
static constexpr auto EnqueueTimeout = std::chrono::milliseconds(1000);
|
||||
static constexpr auto CircuitBreakerCooldown = std::chrono::seconds(2);
|
||||
|
||||
P2PMasterClient& master_client_;
|
||||
const UUID client_id_;
|
||||
const size_t sender_thread_count_;
|
||||
const size_t max_batch_size_;
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
// Used to wake sender threads from retry sleep during Stop().
|
||||
std::mutex stop_mutex_;
|
||||
std::condition_variable stop_cv_;
|
||||
|
||||
std::vector<std::unique_ptr<SenderShard>> shards_;
|
||||
std::vector<std::vector<PendingOp>> batch_buffers_;
|
||||
SyncFailureCallback failure_cb_;
|
||||
|
||||
// Set to true by Stop(drop_pending=true) before waking senders.
|
||||
// Tells CollectBatch to return 0 immediately so senders exit after their
|
||||
// current in-flight SendBatch without processing any further queued ops.
|
||||
std::atomic<bool> drop_on_stop_{false};
|
||||
|
||||
// Circuit breaker via consecutive failure count:
|
||||
// >=0 : active, value = consecutive failures so far
|
||||
// <0 : paused (breaker open), abs(value) = failure count at trip
|
||||
std::atomic<int32_t> consecutive_rpc_failures_{0};
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_manager.h"
|
||||
#include "allocation_strategy.h"
|
||||
|
||||
namespace mooncake {
|
||||
namespace test {
|
||||
class MasterServiceTest;
|
||||
}
|
||||
|
||||
class CentralizedClientManager final : public ClientManager {
|
||||
public:
|
||||
/**
|
||||
* @brief CentralizedClientManager support to alloc buf for memory replic
|
||||
* and support some interfaces about local disk segment
|
||||
* @param client_live_ttl_sec Timeout for HEALTH -> DISCONNECTION
|
||||
* @param client_crashed_ttl_sec Timeout for DISCONNECTION -> CRASHED
|
||||
*/
|
||||
CentralizedClientManager(const int64_t client_live_ttl_sec,
|
||||
const int64_t client_crashed_ttl_sec,
|
||||
const BufferAllocatorType memory_allocator_type,
|
||||
const ViewVersionId view_version);
|
||||
|
||||
auto MountLocalDiskSegment(const UUID& client_id, bool enable_offloading)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
auto OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading)
|
||||
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
|
||||
auto PushOffloadingQueue(const std::string& key, const int64_t size,
|
||||
const std::string& segment_name)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
auto Allocate(const uint64_t slice_length, const size_t replica_num,
|
||||
const std::vector<std::string>& preferred_segments)
|
||||
-> tl::expected<std::vector<Replica>, ErrorCode>;
|
||||
|
||||
protected:
|
||||
DeploymentMode GetDeploymentMode() const override {
|
||||
return DeploymentMode::CENTRALIZATION;
|
||||
}
|
||||
|
||||
std::shared_ptr<ClientMeta> CreateClientMeta(
|
||||
const RegisterClientRequest& req) override;
|
||||
|
||||
HeartbeatTaskResult ProcessTask(const UUID& client_id,
|
||||
const HeartbeatTask& task) override;
|
||||
|
||||
private:
|
||||
BufferAllocatorType memory_allocator_type_;
|
||||
|
||||
// Global allocator manager aggregates allocators from all clients.
|
||||
// Protected by its own mutex, independent of clients_mutex_.
|
||||
mutable SharedMutex global_allocator_mutex_;
|
||||
AllocatorManager global_allocator_manager_
|
||||
GUARDED_BY(global_allocator_mutex_);
|
||||
std::shared_ptr<AllocationStrategy> allocation_strategy_;
|
||||
|
||||
friend class SegmentTest;
|
||||
friend class test::MasterServiceTest;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_meta.h"
|
||||
#include "centralized_segment_manager.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class CentralizedClientMeta : public ClientMeta {
|
||||
public:
|
||||
CentralizedClientMeta(const UUID& client_id,
|
||||
BufferAllocatorType allocator_type);
|
||||
|
||||
std::shared_ptr<SegmentManager> GetSegmentManager() override;
|
||||
std::shared_ptr<CentralizedSegmentManager> GetCentralizedSegmentManager();
|
||||
tl::expected<std::vector<std::string>, ErrorCode> QueryIp(
|
||||
const UUID& client_id) override;
|
||||
|
||||
auto MountLocalDiskSegment(bool enable_offloading)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
auto OffloadObjectHeartbeat(bool enable_offloading)
|
||||
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
|
||||
auto PushOffloadingQueue(const std::string& key, const int64_t size,
|
||||
const std::string& segment_name)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
public:
|
||||
void DoOnDisconnected() override;
|
||||
void DoOnRecovered() override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<CentralizedSegmentManager> segment_manager_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_service.h"
|
||||
#include "centralized_master_client.h"
|
||||
#include "storage_backend.h"
|
||||
#include "file_storage.h"
|
||||
#include "transfer_task.h"
|
||||
#include "thread_pool.h"
|
||||
#include <chrono>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class PutOperation;
|
||||
class FileStorage;
|
||||
|
||||
/**
|
||||
* @brief Centralized-specific query result with lease timeout information
|
||||
*/
|
||||
class CentralizedQueryResult final : public QueryResult {
|
||||
public:
|
||||
/** @brief Time point when the lease for this key expires */
|
||||
const std::chrono::steady_clock::time_point lease_timeout;
|
||||
|
||||
CentralizedQueryResult(
|
||||
std::vector<Replica::Descriptor>&& replicas_param,
|
||||
std::chrono::steady_clock::time_point lease_timeout_param)
|
||||
: QueryResult(std::move(replicas_param)),
|
||||
lease_timeout(lease_timeout_param) {}
|
||||
|
||||
bool IsLeaseExpired() const {
|
||||
return std::chrono::steady_clock::now() >= lease_timeout;
|
||||
}
|
||||
|
||||
bool IsLeaseExpired(std::chrono::steady_clock::time_point& now) const {
|
||||
return now >= lease_timeout;
|
||||
}
|
||||
};
|
||||
|
||||
class CentralizedClientService
|
||||
: public ClientService,
|
||||
public std::enable_shared_from_this<CentralizedClientService> {
|
||||
public:
|
||||
CentralizedClientService(
|
||||
const std::string& local_ip, uint16_t te_port,
|
||||
const std::string& metadata_connstring, uint16_t metrics_port = 9003,
|
||||
bool enable_metrics_http = true,
|
||||
const std::map<std::string, std::string>& labels = {});
|
||||
|
||||
~CentralizedClientService() override;
|
||||
|
||||
ErrorCode Init(const CentralizedClientConfig& config);
|
||||
void Stop() override;
|
||||
void Destroy() override;
|
||||
|
||||
tl::expected<std::unique_ptr<QueryResult>, ErrorCode> Query(
|
||||
const std::string& object_key,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::vector<tl::expected<std::unique_ptr<QueryResult>, ErrorCode>>
|
||||
BatchQuery(const std::vector<std::string>& object_keys,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
tl::expected<bool, ErrorCode> IsExist(const std::string& key) override;
|
||||
|
||||
std::vector<tl::expected<bool, ErrorCode>> BatchIsExist(
|
||||
const std::vector<std::string>& keys) override;
|
||||
|
||||
DeploymentMode deployment_mode() const override {
|
||||
return DeploymentMode::CENTRALIZATION;
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode> BatchReplicaClear(
|
||||
const std::vector<std::string>& object_keys, const UUID& client_id,
|
||||
const std::string& segment_name);
|
||||
|
||||
tl::expected<int64_t, ErrorCode> Get(
|
||||
const std::string& key, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::vector<tl::expected<int64_t, ErrorCode>> BatchGet(
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
const ReadRouteConfig& config = {},
|
||||
bool aggregate_same_segment_task = false) override;
|
||||
|
||||
tl::expected<std::shared_ptr<BufferHandle>, ErrorCode> Get(
|
||||
const std::string& key,
|
||||
std::shared_ptr<ClientBufferAllocator> allocator,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::vector<tl::expected<std::shared_ptr<BufferHandle>, ErrorCode>>
|
||||
BatchGet(const std::vector<std::string>& keys,
|
||||
std::shared_ptr<ClientBufferAllocator> allocator,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
tl::expected<void, ErrorCode> Put(const ObjectKey& key,
|
||||
std::vector<Slice>& slices,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPut(
|
||||
const std::vector<ObjectKey>& keys,
|
||||
std::vector<std::vector<Slice>>& batched_slices,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
tl::expected<void, ErrorCode> Remove(const ObjectKey& key) override;
|
||||
|
||||
tl::expected<long, ErrorCode> RemoveByRegex(const ObjectKey& str) override;
|
||||
|
||||
tl::expected<long, ErrorCode> RemoveAll() override;
|
||||
|
||||
tl::expected<void, ErrorCode> MountSegment(const void* buffer,
|
||||
size_t size) override;
|
||||
|
||||
tl::expected<void, ErrorCode> UnmountSegment(const void* buffer,
|
||||
size_t size) override;
|
||||
|
||||
tl::expected<void, ErrorCode> MountLocalDiskSegment(bool enable_offloading);
|
||||
|
||||
tl::expected<void, ErrorCode> OffloadObjectHeartbeat(
|
||||
bool enable_offloading,
|
||||
std::unordered_map<std::string, int64_t>& offloading_objects);
|
||||
|
||||
tl::expected<void, ErrorCode> BatchPutOffloadObject(
|
||||
const std::string& transfer_engine_addr,
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<uintptr_t>& pointers,
|
||||
const std::unordered_map<std::string, Slice>& batched_slices);
|
||||
|
||||
tl::expected<void, ErrorCode> NotifyOffloadSuccess(
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas);
|
||||
|
||||
tl::expected<RegisterClientResponse, ErrorCode> RegisterClient() override;
|
||||
|
||||
protected:
|
||||
HeartbeatRequest build_heartbeat_request() override;
|
||||
|
||||
MasterClient& GetMasterClient() override { return master_client_; }
|
||||
|
||||
private:
|
||||
void InitTransferSubmitter();
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchGetWhenPreferSameNode(
|
||||
const std::vector<std::string>& object_keys,
|
||||
const std::vector<std::unique_ptr<QueryResult>>& query_results,
|
||||
std::unordered_map<std::string, std::vector<Slice>>& slices);
|
||||
|
||||
tl::expected<void, ErrorCode> InnerGet(const std::string& object_key,
|
||||
const QueryResult& query_result,
|
||||
std::vector<Slice>& slices);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> InnerBatchGet(
|
||||
const std::vector<std::string>& object_keys,
|
||||
const std::vector<std::unique_ptr<QueryResult>>& query_results,
|
||||
std::unordered_map<std::string, std::vector<Slice>>& slices,
|
||||
bool prefer_same_node = false);
|
||||
|
||||
std::vector<PutOperation> CreatePutOperations(
|
||||
const std::vector<ObjectKey>& keys,
|
||||
const std::vector<std::vector<Slice>>& batched_slices);
|
||||
void StartBatchPut(std::vector<PutOperation>& ops,
|
||||
const ReplicateConfig& config);
|
||||
void SubmitTransfers(std::vector<PutOperation>& ops);
|
||||
void WaitForTransfers(std::vector<PutOperation>& ops);
|
||||
void FinalizeBatchPut(std::vector<PutOperation>& ops);
|
||||
std::vector<tl::expected<void, ErrorCode>> CollectResults(
|
||||
const std::vector<PutOperation>& ops);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPutWhenPreferSameNode(
|
||||
std::vector<PutOperation>& ops);
|
||||
|
||||
void PrepareStorageBackend(const std::string& storage_root_dir,
|
||||
const std::string& fsdir,
|
||||
bool enable_eviction = true,
|
||||
uint64_t quota_bytes = 0);
|
||||
|
||||
void PutToLocalFile(const std::string& object_key,
|
||||
const std::vector<Slice>& slices,
|
||||
const DiskDescriptor& disk_descriptor);
|
||||
|
||||
ErrorCode TransferData(const Replica::Descriptor& replica_descriptor,
|
||||
std::vector<Slice>& slices,
|
||||
TransferRequest::OpCode op_code);
|
||||
ErrorCode TransferWrite(const Replica::Descriptor& replica_descriptor,
|
||||
std::vector<Slice>& slices);
|
||||
ErrorCode TransferRead(const Replica::Descriptor& replica_descriptor,
|
||||
std::vector<Slice>& slices);
|
||||
|
||||
tl::expected<void, ErrorCode> InnerUnmountSegment(const void* buffer,
|
||||
size_t size);
|
||||
|
||||
ErrorCode FindFirstCompleteReplica(
|
||||
const std::vector<Replica::Descriptor>& replica_list,
|
||||
Replica::Descriptor& replica);
|
||||
|
||||
private:
|
||||
CentralizedMasterClient master_client_;
|
||||
std::unique_ptr<TransferSubmitter> transfer_submitter_;
|
||||
|
||||
// Mutex to protect mounted_segments_
|
||||
std::mutex mounted_segments_mutex_;
|
||||
std::unordered_map<UUID, Segment, boost::hash<UUID>> mounted_segments_;
|
||||
|
||||
// File storage for offloading
|
||||
std::shared_ptr<FileStorage> file_storage_;
|
||||
|
||||
// Client persistent thread pool for async operations
|
||||
ThreadPool write_thread_pool_;
|
||||
std::shared_ptr<StorageBackend> storage_backend_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
#pragma once
|
||||
|
||||
#include "master_client.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @brief Client for interacting with the mooncake master service
|
||||
*/
|
||||
class CentralizedMasterClient final : public MasterClient {
|
||||
public:
|
||||
CentralizedMasterClient(const UUID& client_id,
|
||||
MasterClientMetric* metrics = nullptr)
|
||||
: MasterClient(client_id, metrics) {}
|
||||
|
||||
CentralizedMasterClient(const CentralizedMasterClient&) = delete;
|
||||
CentralizedMasterClient& operator=(const CentralizedMasterClient&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Starts a put operation
|
||||
* @param key Object key
|
||||
* @param batch_slice_lengths Vector of slice lengths
|
||||
* @param value_length Total value length
|
||||
* @param config Replication configuration
|
||||
* @return tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
|
||||
* indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
|
||||
PutStart(const std::string& key,
|
||||
const std::vector<size_t>& batch_slice_lengths,
|
||||
const ReplicateConfig& config);
|
||||
|
||||
/**
|
||||
* @brief Starts a batch of put operations for N objects
|
||||
* @param keys Vector of object key
|
||||
* @param value_lengths Vector of total value lengths
|
||||
* @param slice_lengths Vector of vectors of slice lengths
|
||||
* @param config Replication configuration
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] std::vector<
|
||||
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
||||
BatchPutStart(const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<uint64_t>>& slice_lengths,
|
||||
const ReplicateConfig& config);
|
||||
|
||||
/**
|
||||
* @brief Ends a put operation
|
||||
* @param key Object key
|
||||
* @param replica_type Type of replica (memory or disk)
|
||||
* @return tl::expected<void, ErrorCode> indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> PutEnd(
|
||||
const std::string& key, ReplicaType replica_type);
|
||||
|
||||
/**
|
||||
* @brief Ends a put operation for a batch of objects
|
||||
* @param keys Vector of object keys
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
|
||||
const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief Revokes a put operation
|
||||
* @param key Object key
|
||||
* @param replica_type Type of replica (memory or disk)
|
||||
* @return tl::expected<void, ErrorCode> indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> PutRevoke(
|
||||
const std::string& key, ReplicaType replica_type);
|
||||
|
||||
/**
|
||||
* @brief Revokes a put operation for a batch of objects
|
||||
* @param keys Vector of object keys
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
|
||||
const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief Batch clear KV cache for specified object keys on a specific
|
||||
* segment for a given client.
|
||||
* @param object_keys Vector of object key strings to clear.
|
||||
* @param client_id The UUID of the client that owns the object keys.
|
||||
* @param segment_name The name of the segment (storage device) to clear
|
||||
* from.
|
||||
* @return An expected object containing a vector of successfully cleared
|
||||
* object keys on success, or an ErrorCode on failure.
|
||||
*/
|
||||
[[nodiscard]] tl::expected<std::vector<std::string>, ErrorCode>
|
||||
BatchReplicaClear(const std::vector<std::string>& object_keys,
|
||||
const UUID& client_id, const std::string& segment_name);
|
||||
|
||||
/**
|
||||
* @brief Gets the cluster ID for the current client to use as subdirectory
|
||||
* name
|
||||
* @return GetClusterIdResponse containing the cluster ID
|
||||
*/
|
||||
[[nodiscard]] tl::expected<std::string, ErrorCode> GetFsdir();
|
||||
|
||||
[[nodiscard]] tl::expected<GetStorageConfigResponse, ErrorCode>
|
||||
GetStorageConfig();
|
||||
|
||||
/**
|
||||
* @brief Mounts a local disk segment into the master.
|
||||
* @param enable_offloading If true, enables offloading (write-to-file).
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> MountLocalDiskSegment(
|
||||
const UUID& client_id, bool enable_offloading);
|
||||
|
||||
/**
|
||||
* @brief Heartbeat call to collect object-level statistics and retrieve the
|
||||
* set of non-persisted objects.
|
||||
* @param enable_offloading Indicates whether persistence is enabled for
|
||||
* this segment.
|
||||
*/
|
||||
[[nodiscard]] tl::expected<std::unordered_map<std::string, int64_t>,
|
||||
ErrorCode>
|
||||
OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading);
|
||||
|
||||
/**
|
||||
* @brief Adds multiple new objects to a specified client in batch.
|
||||
* @param keys A list of object keys (names) that were successfully
|
||||
* offloaded.
|
||||
* @param metadatas The corresponding metadata for each offloaded object,
|
||||
* including size, storage location, etc.
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas);
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,446 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <boost/lockfree/queue.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <ylt/util/expected.hpp>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
|
||||
#include "master_metric_manager.h"
|
||||
#include "master_service.h"
|
||||
#include "mutex.h"
|
||||
#include "centralized_client_manager.h"
|
||||
#include "types.h"
|
||||
#include "rpc_types.h"
|
||||
#include "replica.h"
|
||||
|
||||
namespace mooncake {
|
||||
namespace test {
|
||||
class MasterServiceTest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The CentralizedMasterService is centralized service implementation for
|
||||
* master node. It managers all meta of cluster. The main duty of this master
|
||||
* is:
|
||||
* 1. Key metadata management (such as routing, allocation, eviction in cluster)
|
||||
* 2. Cluster state management (such as segment, client, replica)
|
||||
*/
|
||||
class CentralizedMasterService final : public MasterService {
|
||||
struct CentralizedMetadataShard;
|
||||
|
||||
public:
|
||||
CentralizedMasterService();
|
||||
explicit CentralizedMasterService(const MasterServiceConfig& config);
|
||||
~CentralizedMasterService() override;
|
||||
|
||||
auto GetReplicaList(const std::string& key,
|
||||
const GetReplicaListRequestConfig& config =
|
||||
GetReplicaListRequestConfig())
|
||||
-> tl::expected<GetReplicaListResponse, ErrorCode> override;
|
||||
|
||||
/**
|
||||
* @brief Start a put operation for an object
|
||||
* @param[out] replica_list Vector to store replica information for the
|
||||
* slice
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if exists,
|
||||
* ErrorCode::NO_AVAILABLE_HANDLE if allocation fails,
|
||||
* ErrorCode::INVALID_PARAMS if slice size is invalid
|
||||
*/
|
||||
auto PutStart(const UUID& client_id, const std::string& key,
|
||||
const uint64_t slice_length, const ReplicateConfig& config)
|
||||
-> tl::expected<std::vector<Replica::Descriptor>, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Complete a put operation, replica_type indicates the type of
|
||||
* replica to complete (memory or disk)
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* found, ErrorCode::INVALID_WRITE if replica status is invalid
|
||||
*/
|
||||
auto PutEnd(const UUID& client_id, const std::string& key,
|
||||
ReplicaType replica_type) -> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Complete a batch of put operations
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* found, ErrorCode::INVALID_WRITE if replica status is invalid
|
||||
*/
|
||||
auto BatchPutEnd(const UUID& client_id,
|
||||
const std::vector<std::string>& keys)
|
||||
-> std::vector<tl::expected<void, ErrorCode>>;
|
||||
|
||||
/**
|
||||
* @brief Revoke a put operation, replica_type indicates the type of
|
||||
* replica to revoke (memory or disk)
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* found, ErrorCode::INVALID_WRITE if replica status is invalid
|
||||
*/
|
||||
auto PutRevoke(const UUID& client_id, const std::string& key,
|
||||
ReplicaType replica_type) -> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Revoke a batch of put operations
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* found, ErrorCode::INVALID_WRITE if replica status is invalid
|
||||
*/
|
||||
auto BatchPutRevoke(const UUID& client_id,
|
||||
const std::vector<std::string>& keys)
|
||||
-> std::vector<tl::expected<void, ErrorCode>>;
|
||||
|
||||
/**
|
||||
* @brief Batch clear KV cache replicas for specified object keys.
|
||||
* @param object_keys Vector of object key strings to clear.
|
||||
* @param client_id The UUID of the client that owns the object keys.
|
||||
* @param segment_name The name of the segment (storage device) to clear
|
||||
* from. If empty, clears replicas from all segments for the given
|
||||
* client_id.
|
||||
* @return An expected object containing a vector of successfully cleared
|
||||
* keys on success, or an ErrorCode on failure. Only successfully
|
||||
* cleared keys are included in the result.
|
||||
*/
|
||||
auto BatchReplicaClear(const std::vector<std::string>& object_keys,
|
||||
const UUID& client_id,
|
||||
const std::string& segment_name)
|
||||
-> tl::expected<std::vector<std::string>, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Adds a replica instance associated with the given client and key.
|
||||
*/
|
||||
auto AddReplica(const UUID& client_id, const std::string& key,
|
||||
Replica& replica) -> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Get the master service cluster ID to use as subdirectory name
|
||||
* @return ErrorCode::OK on success, ErrorCode::INTERNAL_ERROR if cluster ID
|
||||
* is not set
|
||||
*/
|
||||
tl::expected<std::string, ErrorCode> GetFsdir() const;
|
||||
|
||||
/**
|
||||
* @brief Get storage backend configuration including eviction settings
|
||||
* @return GetStorageConfigResponse containing fsdir, enable_disk_eviction,
|
||||
* and quota_bytes
|
||||
*/
|
||||
tl::expected<GetStorageConfigResponse, ErrorCode> GetStorageConfig() const;
|
||||
|
||||
/**
|
||||
* @brief Mounts a file storage segment into the master.
|
||||
* @param enable_offloading If true, enables offloading (write-to-file).
|
||||
*/
|
||||
auto MountLocalDiskSegment(const UUID& client_id, bool enable_offloading)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Heartbeat call to collect object-level statistics and retrieve the
|
||||
* set of non-offloaded objects.
|
||||
* @param enable_offloading Indicates whether offloading is enabled for this
|
||||
* segment.
|
||||
*/
|
||||
auto OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading)
|
||||
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Notifies the master that offloading of specified objects has
|
||||
* succeeded.
|
||||
* @param keys A list of object keys (names) that were successfully
|
||||
* offloaded.
|
||||
* @param metadatas The corresponding metadata for each offloaded object,
|
||||
* including size, storage location, etc.
|
||||
*/
|
||||
auto NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
public:
|
||||
ClientManager& GetClientManager() override { return client_manager_; }
|
||||
|
||||
const ClientManager& GetClientManager() const override {
|
||||
return client_manager_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Replica::Descriptor> FilterReplicas(
|
||||
const GetReplicaListRequestConfig& config,
|
||||
const ObjectMetadata& metadata) override;
|
||||
|
||||
// Hooks implementation
|
||||
void OnObjectAccessed(ObjectMetadata& metadata) override;
|
||||
void OnObjectHit(const ObjectMetadata& metadata) override;
|
||||
void OnReplicaRemoved(const Replica& replica) override;
|
||||
void OnReplicaAdded(const Replica& replica) override;
|
||||
|
||||
void OnSegmentRemoved(const UUID& segment_id) override;
|
||||
|
||||
private:
|
||||
// Resolve the key to a sanitized format for storage
|
||||
std::string SanitizeKey(const std::string& key) const;
|
||||
std::string ResolvePath(const std::string& key) const;
|
||||
|
||||
// BatchEvict evicts objects in a near-LRU way, i.e., prioritizes to evict
|
||||
// object with smaller lease timeout. It has two passes. The first pass only
|
||||
// evicts objects without soft pin. The second pass prioritizes objects
|
||||
// without soft pin, but also allows to evict soft pinned objects if
|
||||
// allow_evict_soft_pinned_objects_ is true. The first pass tries fulfill
|
||||
// evict ratio target. If the actual evicted ratio is less than
|
||||
// evict_ratio_lowerbound, the second pass will be triggered and try to
|
||||
// fulfill evict ratio lowerbound.
|
||||
void BatchEvict(double evict_ratio_target, double evict_ratio_lowerbound);
|
||||
|
||||
/**
|
||||
* @brief Helper to discard expired processing keys.
|
||||
*/
|
||||
void DiscardExpiredProcessingKeys(
|
||||
CentralizedMetadataShard& shard,
|
||||
const std::chrono::steady_clock::time_point& now);
|
||||
|
||||
/**
|
||||
* @brief Helper to release space of expired discarded replicas.
|
||||
* @return Number of released objects that have memory replicas
|
||||
*/
|
||||
uint64_t ReleaseExpiredDiscardedReplicas(
|
||||
const std::chrono::steady_clock::time_point& now);
|
||||
|
||||
// Eviction thread function
|
||||
void EvictionThreadFunc();
|
||||
|
||||
tl::expected<void, ErrorCode> PushOffloadingQueue(const std::string& key,
|
||||
const Replica& replica);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief CentralizedObjectMetadata extends ObjectMetadata with lease and
|
||||
* ReplicaStatus management for centralized master service.
|
||||
*/
|
||||
struct CentralizedObjectMetadata final : public ObjectMetadata {
|
||||
public:
|
||||
CentralizedObjectMetadata(
|
||||
const UUID& client_id,
|
||||
const std::chrono::steady_clock::time_point put_start_time,
|
||||
size_t value_length, std::vector<Replica>&& reps,
|
||||
bool enable_soft_pin);
|
||||
|
||||
~CentralizedObjectMetadata() override;
|
||||
|
||||
CentralizedObjectMetadata(const CentralizedObjectMetadata&) = delete;
|
||||
CentralizedObjectMetadata& operator=(const CentralizedObjectMetadata&) =
|
||||
delete;
|
||||
CentralizedObjectMetadata(CentralizedObjectMetadata&&) = delete;
|
||||
CentralizedObjectMetadata& operator=(CentralizedObjectMetadata&&) =
|
||||
delete;
|
||||
|
||||
// Check if there are some replicas with a different status than the
|
||||
// given value. If there are, return the status of the first replica
|
||||
// that is not equal to the given value. Otherwise, return std::nullopt.
|
||||
std::optional<ReplicaStatus> HasDiffRepStatus(
|
||||
ReplicaStatus status, ReplicaType replica_type) const;
|
||||
|
||||
// Grant a lease with timeout as now() + ttl, only update if the new
|
||||
// timeout is larger
|
||||
void GrantLease(const uint64_t ttl, const uint64_t soft_ttl);
|
||||
|
||||
// Erase all replicas of the given type
|
||||
void EraseReplica(ReplicaType type);
|
||||
|
||||
// Check if there is a memory replica
|
||||
bool HasMemReplica() const;
|
||||
|
||||
// Get the count of memory replicas
|
||||
int GetMemReplicaCount() const;
|
||||
|
||||
// Check if the lease has expired
|
||||
bool IsLeaseExpired() const;
|
||||
|
||||
// Check if the lease has expired
|
||||
bool IsLeaseExpired(
|
||||
const std::chrono::steady_clock::time_point& now) const;
|
||||
|
||||
// Check if is in soft pin status
|
||||
bool IsSoftPinned() const;
|
||||
|
||||
// Check if is in soft pin status
|
||||
bool IsSoftPinned(
|
||||
const std::chrono::steady_clock::time_point& now) const;
|
||||
|
||||
// Check if all replicas are complete
|
||||
bool IsAllReplicasComplete() const;
|
||||
|
||||
// Check if has any completed replicas
|
||||
bool HasCompletedReplicas() const;
|
||||
|
||||
// Discard all processing replicas and return them
|
||||
std::vector<Replica> DiscardProcessingReplicas();
|
||||
|
||||
public:
|
||||
// Hook functions
|
||||
tl::expected<void, ErrorCode> IsObjectRemovable() const override;
|
||||
bool IsReplicaAccessible(const Replica& replica) const override;
|
||||
tl::expected<void, ErrorCode> IsReplicaRemovable(
|
||||
const Replica& replica) const override;
|
||||
|
||||
public:
|
||||
// The client that created this object (via PutStart).
|
||||
const UUID owner_client_id_;
|
||||
const std::chrono::steady_clock::time_point put_start_time_;
|
||||
std::chrono::steady_clock::time_point lease_timeout_;
|
||||
std::optional<std::chrono::steady_clock::time_point> soft_pin_timeout_;
|
||||
};
|
||||
|
||||
private:
|
||||
// Extended MetadataShard with processing_keys for centralized service
|
||||
struct CentralizedMetadataShard : public MetadataShard {
|
||||
// Keys currently being written (PutStart called, but not yet PutEnd)
|
||||
// Protected by the inherited mutex from MetadataShard
|
||||
std::unordered_set<std::string> processing_keys GUARDED_BY(mutex);
|
||||
};
|
||||
|
||||
// Override GetShard to return our extended shard type
|
||||
MetadataShard& GetShard(size_t idx) override {
|
||||
return metadata_shards_[idx];
|
||||
}
|
||||
const MetadataShard& GetShard(size_t idx) const override {
|
||||
return metadata_shards_[idx];
|
||||
}
|
||||
|
||||
// Helper to get the extended shard with processing_keys
|
||||
CentralizedMetadataShard& GetCentralizedShard(size_t idx) {
|
||||
return metadata_shards_[idx];
|
||||
}
|
||||
const CentralizedMetadataShard& GetCentralizedShard(size_t idx) const {
|
||||
return metadata_shards_[idx];
|
||||
}
|
||||
static constexpr size_t kNumShards = 1024; // Number of metadata shards
|
||||
// Helper to get shard index from key
|
||||
size_t GetShardIndex(const std::string& key) const override {
|
||||
return std::hash<std::string>{}(key) % kNumShards;
|
||||
}
|
||||
size_t GetShardCount() const override { return kNumShards; }
|
||||
|
||||
private:
|
||||
class CentralizedMetadataAccessor final
|
||||
: public MasterService::MetadataAccessor {
|
||||
public:
|
||||
CentralizedMetadataAccessor(CentralizedMasterService* service,
|
||||
const std::string& key)
|
||||
: MasterService::MetadataAccessor(service, key),
|
||||
c_shard_(static_cast<CentralizedMetadataShard&>(shard_)),
|
||||
processing_it_(c_shard_.processing_keys.find(key)) {}
|
||||
|
||||
CentralizedObjectMetadata& Get() NO_THREAD_SAFETY_ANALYSIS {
|
||||
return static_cast<CentralizedObjectMetadata&>(
|
||||
MasterService::MetadataAccessor::Get());
|
||||
}
|
||||
|
||||
bool InProcessing() const NO_THREAD_SAFETY_ANALYSIS {
|
||||
return processing_it_ != c_shard_.processing_keys.end();
|
||||
}
|
||||
|
||||
void EraseFromProcessing() NO_THREAD_SAFETY_ANALYSIS {
|
||||
c_shard_.processing_keys.erase(processing_it_);
|
||||
processing_it_ = c_shard_.processing_keys.end();
|
||||
}
|
||||
|
||||
// Access the extended shard directly (for inserting processing keys)
|
||||
CentralizedMetadataShard& GetCentralizedShard() { return c_shard_; }
|
||||
|
||||
private:
|
||||
CentralizedMetadataShard& c_shard_;
|
||||
std::unordered_set<std::string>::iterator processing_it_;
|
||||
};
|
||||
|
||||
std::unique_ptr<MetadataAccessor> GetMetadataAccessor(
|
||||
const std::string& key) override {
|
||||
return std::make_unique<CentralizedMetadataAccessor>(this, key);
|
||||
}
|
||||
|
||||
private:
|
||||
class DiscardedReplicas {
|
||||
public:
|
||||
DiscardedReplicas() = delete;
|
||||
|
||||
DiscardedReplicas(std::vector<Replica>&& replicas,
|
||||
std::chrono::steady_clock::time_point ttl)
|
||||
: replicas_(std::move(replicas)), ttl_(ttl), mem_size_(0) {
|
||||
for (auto& replica : replicas_) {
|
||||
mem_size_ += replica.get_memory_buffer_size();
|
||||
}
|
||||
MasterMetricManager::instance().inc_put_start_discard_cnt(
|
||||
1, mem_size_);
|
||||
}
|
||||
|
||||
~DiscardedReplicas() {
|
||||
MasterMetricManager::instance().inc_put_start_release_cnt(
|
||||
1, mem_size_);
|
||||
}
|
||||
|
||||
uint64_t memSize() const { return mem_size_; }
|
||||
|
||||
bool isExpired(const std::chrono::steady_clock::time_point& now) const {
|
||||
return ttl_ <= now;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Replica> replicas_;
|
||||
std::chrono::steady_clock::time_point ttl_;
|
||||
uint64_t mem_size_;
|
||||
};
|
||||
Mutex discarded_replicas_mutex_;
|
||||
std::list<DiscardedReplicas> discarded_replicas_
|
||||
GUARDED_BY(discarded_replicas_mutex_);
|
||||
|
||||
private:
|
||||
std::array<CentralizedMetadataShard, kNumShards> metadata_shards_;
|
||||
|
||||
// Lease related members
|
||||
const uint64_t default_kv_lease_ttl_; // in milliseconds
|
||||
const uint64_t default_kv_soft_pin_ttl_; // in milliseconds
|
||||
const bool allow_evict_soft_pinned_objects_;
|
||||
|
||||
// Eviction related members
|
||||
std::atomic<bool> need_eviction_{
|
||||
false}; // Set to trigger eviction when not enough space left
|
||||
const double eviction_ratio_; // in range [0.0, 1.0]
|
||||
const double eviction_high_watermark_ratio_; // in range [0.0, 1.0]
|
||||
|
||||
// Eviction thread related members
|
||||
std::thread eviction_thread_;
|
||||
std::atomic<bool> eviction_running_{false};
|
||||
static constexpr uint64_t kEvictionThreadSleepMs =
|
||||
10; // 10 ms sleep between eviction checks
|
||||
|
||||
const bool enable_offload_;
|
||||
|
||||
// cluster id for persistent sub directory
|
||||
const std::string cluster_id_;
|
||||
// root filesystem directory for persistent storage
|
||||
const std::string root_fs_dir_;
|
||||
// global 3fs/nfs segment size
|
||||
int64_t global_file_segment_size_;
|
||||
// storage backend eviction configuration
|
||||
const bool enable_disk_eviction_;
|
||||
const uint64_t quota_bytes_;
|
||||
|
||||
bool use_disk_replica_{false};
|
||||
|
||||
// Segment management
|
||||
CentralizedClientManager client_manager_;
|
||||
BufferAllocatorType memory_allocator_type_;
|
||||
|
||||
// Discarded replicas management
|
||||
const std::chrono::seconds put_start_discard_timeout_sec_;
|
||||
const std::chrono::seconds put_start_release_timeout_sec_;
|
||||
friend class CentralizedMetadataAccessor;
|
||||
friend class test::MasterServiceTest;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
#pragma once
|
||||
|
||||
#include "rpc_service.h"
|
||||
#include "centralized_master_service.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class WrappedCentralizedMasterService final : public WrappedMasterService {
|
||||
public:
|
||||
WrappedCentralizedMasterService(const WrappedMasterServiceConfig& config);
|
||||
|
||||
// Initialize centralized-specific HTTP handlers
|
||||
void init_centralized_http_server();
|
||||
|
||||
tl::expected<std::vector<Replica::Descriptor>, ErrorCode> PutStart(
|
||||
const UUID& client_id, const std::string& key,
|
||||
const uint64_t slice_length, const ReplicateConfig& config);
|
||||
|
||||
tl::expected<void, ErrorCode> PutEnd(const UUID& client_id,
|
||||
const std::string& key,
|
||||
ReplicaType replica_type);
|
||||
|
||||
tl::expected<void, ErrorCode> PutRevoke(const UUID& client_id,
|
||||
const std::string& key,
|
||||
ReplicaType replica_type);
|
||||
|
||||
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
||||
BatchPutStart(const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<uint64_t>& slice_lengths,
|
||||
const ReplicateConfig& config);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
|
||||
const UUID& client_id, const std::vector<std::string>& keys);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
|
||||
const UUID& client_id, const std::vector<std::string>& keys);
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode> BatchReplicaClear(
|
||||
const std::vector<std::string>& object_keys, const UUID& client_id,
|
||||
const std::string& segment_name);
|
||||
|
||||
tl::expected<std::string, ErrorCode> GetFsdir();
|
||||
|
||||
tl::expected<GetStorageConfigResponse, ErrorCode> GetStorageConfig();
|
||||
|
||||
tl::expected<void, ErrorCode> MountLocalDiskSegment(const UUID& client_id,
|
||||
bool enable_offloading);
|
||||
|
||||
tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>
|
||||
OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading);
|
||||
|
||||
tl::expected<void, ErrorCode> NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas);
|
||||
|
||||
protected:
|
||||
virtual MasterService& GetMasterService() override {
|
||||
return master_service_;
|
||||
}
|
||||
|
||||
private:
|
||||
CentralizedMasterService master_service_;
|
||||
};
|
||||
|
||||
void RegisterCentralizedRpcService(
|
||||
coro_rpc::coro_rpc_server& server,
|
||||
mooncake::WrappedCentralizedMasterService& wrapped_master_service);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
#pragma once
|
||||
|
||||
#include "allocator.h"
|
||||
#include "segment_manager.h"
|
||||
#include <boost/functional/hash.hpp>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
// Although MountedCentralizedSegments are managed locally,
|
||||
// buf_allocator of them are also registered to the upper-level
|
||||
// CentralizedClientManager's global_allocator_manager_ via
|
||||
// AllocatorChangeCallback, so that the Allocate() of segment can work globally.
|
||||
struct MountedCentralizedSegment : public Segment {
|
||||
std::shared_ptr<BufferAllocatorBase> buf_allocator;
|
||||
};
|
||||
|
||||
struct LocalDiskSegment {
|
||||
mutable Mutex offloading_mutex_;
|
||||
bool enable_offloading;
|
||||
std::unordered_map<std::string, int64_t> GUARDED_BY(offloading_mutex_)
|
||||
offloading_objects;
|
||||
explicit LocalDiskSegment(bool enable_offloading)
|
||||
: enable_offloading(enable_offloading) {}
|
||||
|
||||
LocalDiskSegment(const LocalDiskSegment&) = delete;
|
||||
LocalDiskSegment& operator=(const LocalDiskSegment&) = delete;
|
||||
|
||||
LocalDiskSegment(LocalDiskSegment&&) = delete;
|
||||
LocalDiskSegment& operator=(LocalDiskSegment&&) = delete;
|
||||
};
|
||||
|
||||
class CentralizedSegmentManager : public SegmentManager {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor for CentralizedSegmentManager
|
||||
* @param memory_allocator Type of buffer allocator to use for new segments
|
||||
*/
|
||||
explicit CentralizedSegmentManager(
|
||||
BufferAllocatorType memory_allocator = BufferAllocatorType::OFFSET)
|
||||
: memory_allocator_(memory_allocator) {}
|
||||
|
||||
/**
|
||||
* @brief Callback for allocator changes (add/remove).
|
||||
* Registered by CentralizedClientManager to sync allocators with its
|
||||
* global_allocator_manager_, enabling global Allocate() without
|
||||
* iterating per-client segment managers.
|
||||
* @param segment_name Name of the segment
|
||||
* @param allocator The allocator being added or removed
|
||||
* @param is_add true if allocator is being added, false if removed
|
||||
* @return ErrorCode::OK on success
|
||||
*/
|
||||
using AllocatorChangeCallback = std::function<tl::expected<void, ErrorCode>(
|
||||
const std::string& segment_name,
|
||||
const std::shared_ptr<BufferAllocatorBase>& allocator, bool is_add)>;
|
||||
|
||||
void SetAllocatorChangeCallback(AllocatorChangeCallback cb);
|
||||
|
||||
// Set the global visibility of all segments managed by this manager.
|
||||
// When visible is true, all segments are added to the global allocator
|
||||
// manager. When visible is false, all segments are removed from the
|
||||
// global allocator manager.
|
||||
tl::expected<void, ErrorCode> SetGlobalVisibility(bool visible);
|
||||
|
||||
auto QuerySegments(const std::string& segment)
|
||||
-> tl::expected<std::pair<size_t, size_t>, ErrorCode> override;
|
||||
|
||||
auto MountLocalDiskSegment(bool enable_offloading)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
auto OffloadObjectHeartbeat(bool enable_offloading)
|
||||
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
|
||||
auto PushOffloadingQueue(const std::string& key, const int64_t size,
|
||||
const std::string& segment_name)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
auto QueryIp() -> tl::expected<std::vector<std::string>, ErrorCode>;
|
||||
|
||||
protected:
|
||||
ErrorCode InnerCheckMountSegment(const Segment& segment);
|
||||
tl::expected<void, ErrorCode> InnerMountSegment(
|
||||
const Segment& segment) override;
|
||||
auto OnUnmountSegment(const std::shared_ptr<Segment>& segment)
|
||||
-> tl::expected<void, ErrorCode> override;
|
||||
|
||||
private:
|
||||
static constexpr size_t OFFLOADING_QUEUE_LIMIT = 50000;
|
||||
|
||||
const BufferAllocatorType
|
||||
memory_allocator_; // Type of buffer allocator to use
|
||||
AllocatorChangeCallback allocator_change_cb_;
|
||||
std::shared_ptr<LocalDiskSegment> local_disk_segment_
|
||||
GUARDED_BY(segment_mutex_);
|
||||
|
||||
friend class SegmentTest; // for unit tests
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -115,7 +115,17 @@ uint64_t calculate_total_size(const Replica::Descriptor& replica);
|
|||
* @return 0 on success, non-zero on error
|
||||
*/
|
||||
int allocateSlices(std::vector<Slice>& slices,
|
||||
const Replica::Descriptor& replica,
|
||||
void* buffer_ptr);
|
||||
const Replica::Descriptor& replica, void* buffer_ptr);
|
||||
|
||||
/**
|
||||
* @brief Build slices from user-provided buffers, trimmed to total_size.
|
||||
* @param buffers Vector of buffer pointers.
|
||||
* @param sizes Vector of buffer sizes (must match buffers.size()).
|
||||
* @param total_size Maximum total bytes across all slices.
|
||||
* @return Vector of slices whose combined size equals total_size.
|
||||
*/
|
||||
std::vector<Slice> BuildSlicesFromBuffers(const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
uint64_t total_size);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <json/json.h>
|
||||
#include "common.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class TransferEngine;
|
||||
|
||||
// ============================================================================
|
||||
// Config classes
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Configuration for a dummy client deployment.
|
||||
*
|
||||
* A dummy client communicates with a separately deployed real client via
|
||||
* shared memory and RPC. It does not directly interact with the master or
|
||||
* transfer engine.
|
||||
*/
|
||||
struct DummyClientConfig {
|
||||
// Size of the memory pool in bytes.
|
||||
size_t mem_pool_size = 0;
|
||||
|
||||
// Size of the local buffer in bytes.
|
||||
// The local buffer will be registered as shm and shared to real client.
|
||||
size_t local_buffer_size = 0;
|
||||
|
||||
// RPC connection string to real client ("ip:port").
|
||||
std::string real_client_addr;
|
||||
|
||||
// The IPC socket path between dummy and real client.
|
||||
std::string ipc_socket_path;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Common base configuration shared by centralized and P2P real clients.
|
||||
*
|
||||
* Contains all fields needed for transfer engine initialization, master
|
||||
* connection, segment mounting, and local buffer registration.
|
||||
*/
|
||||
struct RealClientConfigBase {
|
||||
// Local IP address
|
||||
std::string local_ip;
|
||||
|
||||
// Transfer engine port (0 means randomly assigned)
|
||||
uint16_t te_port = 0;
|
||||
|
||||
/**
|
||||
* @brief Returns the "ip:port" endpoint string.
|
||||
*/
|
||||
std::string local_endpoint() const {
|
||||
return local_ip + ":" + std::to_string(te_port);
|
||||
}
|
||||
|
||||
// Connection string for metadata service
|
||||
std::string metadata_connstring;
|
||||
|
||||
// Transport protocol (e.g., "tcp", "rdma", "ascend").
|
||||
std::string protocol = "tcp";
|
||||
|
||||
// Comma-separated RDMA device names.
|
||||
// Optional with default auto-discovery.
|
||||
// Only required when auto-discovery is disabled
|
||||
// (set env `MC_MS_AUTO_DISC=0`).
|
||||
std::optional<std::string> rdma_devices = std::nullopt;
|
||||
|
||||
// The entry of master server:
|
||||
// 1. "IP:Port" for non-HA mode
|
||||
// 2. "etcd://IP:Port;...;IP:Port" for HA mode
|
||||
std::string master_server_entry = "127.0.0.1:50051";
|
||||
|
||||
// Size of the local buffer (0 to skip).
|
||||
// For the case which separately deploys real client and dummy client,
|
||||
// the `local_buffer_size` could be 0, which means the local buffer is
|
||||
// shared by dummy client.
|
||||
// For the case which integrates real client,
|
||||
// if the `local_buffer_size` is 0, some interfaces might fail to work.
|
||||
uint64_t local_buffer_size = 0;
|
||||
|
||||
// Optional metric labels for the client
|
||||
std::map<std::string, std::string> labels = {};
|
||||
|
||||
// Optional TransferEngine instance.
|
||||
// If not provided, it will be created by client_service.
|
||||
std::shared_ptr<TransferEngine> transfer_engine = nullptr;
|
||||
|
||||
// The IPC socket path between dummy and real clients.
|
||||
// If use integrated deployment, this could be empty.
|
||||
std::string ipc_socket_path;
|
||||
|
||||
// Port for metrics HTTP server.
|
||||
// Only used when enable_metrics_http is true.
|
||||
uint16_t metrics_port = 9003;
|
||||
|
||||
// Whether to enable metrics HTTP server.
|
||||
// The metrics server exposes /metrics, /metrics/summary, and /health
|
||||
// endpoints.
|
||||
bool enable_metrics_http = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Configuration for a centralized real client.
|
||||
*
|
||||
* Inherits all common real client fields and adds centralized-specific options.
|
||||
*/
|
||||
struct CentralizedClientConfig : RealClientConfigBase {
|
||||
// Size of global segment to mount (0 to skip)
|
||||
uint64_t global_segment_size = 0;
|
||||
|
||||
// Whether to enable file storage offloading.
|
||||
bool enable_offload = false;
|
||||
};
|
||||
|
||||
enum class LocalTransferMode {
|
||||
MEMCPY = 0,
|
||||
TE = 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Configuration for a P2P real client.
|
||||
*
|
||||
* Inherits all common real client fields and adds P2P-specific options.
|
||||
*/
|
||||
struct P2PClientConfig : RealClientConfigBase {
|
||||
// Port for P2P RPC service.
|
||||
uint16_t client_rpc_port = 12345;
|
||||
|
||||
// Num threads for P2P RPC service.
|
||||
uint32_t rpc_thread_num = 2;
|
||||
|
||||
// Parsed custom tiered backend configuration
|
||||
Json::Value tiered_backend_config;
|
||||
|
||||
// Number of key lock shards for DataManager.
|
||||
// Higher values reduce contention of key.
|
||||
size_t lock_shard_count = 1024;
|
||||
|
||||
// RouteCache configuration
|
||||
// each size of route entry is about 240B:
|
||||
// Aligned Node(64B) + hash_bucket(8B) + Key(assume 64B)
|
||||
// + P2PRouteData(each item is 96B and count is 8B)
|
||||
size_t route_cache_max_memory_bytes = 300 * 1024 * 1024; // 300MB
|
||||
uint64_t route_cache_ttl_ms = 5 * 60 * 1000; // 5min
|
||||
|
||||
// Async route notification.
|
||||
// async_sender_thread_count > 0 enables async notifier.
|
||||
// async_route_queue_size controls queue capacity
|
||||
// (minimum async_max_batch_size * async_sender_thread_count).
|
||||
size_t async_sender_thread_count = 0;
|
||||
size_t async_max_batch_size = 2000;
|
||||
size_t async_route_queue_size = 0;
|
||||
|
||||
// Local transfer mode for P2P local Get/Put path.
|
||||
// - MEMCPY: copy through local CPU memory path
|
||||
// - TE: transfer through local TransferEngine path
|
||||
LocalTransferMode local_transfer_mode = LocalTransferMode::TE;
|
||||
|
||||
// When local_transfer_mode == MEMCPY, the following parameter is used:
|
||||
// 0 means forbid async memcpy (fall back to synchronous).
|
||||
size_t local_memcpy_async_worker_num = 32;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Factory class
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Factory class for building typed client configurations.
|
||||
*/
|
||||
class ClientConfigBuilder {
|
||||
public:
|
||||
static DummyClientConfig build_dummy(size_t mem_pool_size,
|
||||
size_t local_buffer_size,
|
||||
const std::string& real_client_addr,
|
||||
const std::string& ipc_socket_path) {
|
||||
DummyClientConfig config;
|
||||
config.mem_pool_size = mem_pool_size;
|
||||
config.local_buffer_size = local_buffer_size;
|
||||
config.real_client_addr = real_client_addr;
|
||||
config.ipc_socket_path = ipc_socket_path;
|
||||
return config;
|
||||
}
|
||||
|
||||
static CentralizedClientConfig build_centralized_real_client(
|
||||
const std::string& local_hostname,
|
||||
const std::string& metadata_connstring,
|
||||
const std::string& protocol = "tcp",
|
||||
const std::optional<std::string>& rdma_devices = std::nullopt,
|
||||
const std::string& master_server_entry = "127.0.0.1:50051",
|
||||
uint64_t global_segment_size = 0, uint64_t local_buffer_size = 0,
|
||||
const std::shared_ptr<TransferEngine>& transfer_engine = nullptr,
|
||||
const std::string& ipc_socket_path = "", bool enable_offload = false,
|
||||
uint16_t metrics_port = 9003, bool enable_metrics_http = true,
|
||||
const std::map<std::string, std::string>& labels = {}) {
|
||||
CentralizedClientConfig config;
|
||||
fill_real_client_config_base(
|
||||
config, local_hostname, metadata_connstring, protocol, rdma_devices,
|
||||
master_server_entry, local_buffer_size, transfer_engine,
|
||||
ipc_socket_path, metrics_port, enable_metrics_http, labels);
|
||||
config.global_segment_size = global_segment_size;
|
||||
config.enable_offload = enable_offload;
|
||||
return config;
|
||||
}
|
||||
|
||||
static P2PClientConfig build_p2p_real_client(
|
||||
const std::string& local_hostname,
|
||||
const std::string& metadata_connstring,
|
||||
const std::string& protocol = "tcp",
|
||||
const std::optional<std::string>& rdma_devices = std::nullopt,
|
||||
const std::string& master_server_entry = "127.0.0.1:50051",
|
||||
const std::string& tiered_backend_config_json = "",
|
||||
uint64_t local_buffer_size = 0,
|
||||
const std::shared_ptr<TransferEngine>& transfer_engine = nullptr,
|
||||
const std::string& ipc_socket_path = "",
|
||||
uint16_t client_rpc_port = 12345, uint32_t rpc_thread_num = 2,
|
||||
size_t lock_shard_count = 1024,
|
||||
size_t route_cache_max_memory_bytes = 300 * 1024 * 1024,
|
||||
uint64_t route_cache_ttl_ms = 5 * 60 * 1000,
|
||||
const std::string& local_transfer_mode = "te",
|
||||
size_t local_memcpy_async_worker_num = 32, uint16_t metrics_port = 9003,
|
||||
bool enable_metrics_http = true,
|
||||
const std::map<std::string, std::string>& labels = {},
|
||||
size_t async_sender_thread_count = 0,
|
||||
size_t async_max_batch_size = 2000, size_t async_route_queue_size = 0) {
|
||||
P2PClientConfig config;
|
||||
fill_real_client_config_base(
|
||||
config, local_hostname, metadata_connstring, protocol, rdma_devices,
|
||||
master_server_entry, local_buffer_size, transfer_engine,
|
||||
ipc_socket_path, metrics_port, enable_metrics_http, labels);
|
||||
config.client_rpc_port = client_rpc_port;
|
||||
config.rpc_thread_num = rpc_thread_num;
|
||||
config.lock_shard_count = lock_shard_count;
|
||||
config.route_cache_max_memory_bytes = route_cache_max_memory_bytes;
|
||||
config.route_cache_ttl_ms = route_cache_ttl_ms;
|
||||
config.local_transfer_mode =
|
||||
parse_p2p_local_transfer_mode(local_transfer_mode);
|
||||
if (config.local_transfer_mode == LocalTransferMode::MEMCPY) {
|
||||
config.local_memcpy_async_worker_num =
|
||||
local_memcpy_async_worker_num;
|
||||
}
|
||||
config.async_sender_thread_count = async_sender_thread_count;
|
||||
config.async_max_batch_size = async_max_batch_size;
|
||||
config.async_route_queue_size = async_route_queue_size;
|
||||
|
||||
Json::Value tiered_config;
|
||||
std::string actual_json = tiered_backend_config_json;
|
||||
if (actual_json.empty()) {
|
||||
if (const char* env_p = std::getenv("MOONCAKE_TIERED_CONFIG")) {
|
||||
actual_json = env_p;
|
||||
}
|
||||
}
|
||||
|
||||
if (!actual_json.empty()) {
|
||||
Json::CharReaderBuilder builder;
|
||||
auto reader =
|
||||
std::unique_ptr<Json::CharReader>(builder.newCharReader());
|
||||
std::string errors;
|
||||
if (!reader->parse(actual_json.data(),
|
||||
actual_json.data() + actual_json.length(),
|
||||
&tiered_config, &errors)) {
|
||||
LOG(ERROR) << "Failed to parse tiered config: " << errors;
|
||||
}
|
||||
}
|
||||
|
||||
if (tiered_config.isNull() || !tiered_config.isMember("tiers") ||
|
||||
tiered_config["tiers"].empty()) {
|
||||
throw std::runtime_error(
|
||||
"Tiered backend configuration is missing. Please provide "
|
||||
"tiered_backend_config_json or set MOONCAKE_TIERED_CONFIG "
|
||||
"environment variable.");
|
||||
}
|
||||
config.tiered_backend_config = tiered_config;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private:
|
||||
static void fill_real_client_config_base(
|
||||
RealClientConfigBase& config, const std::string& local_hostname,
|
||||
const std::string& metadata_connstring, const std::string& protocol,
|
||||
const std::optional<std::string>& rdma_devices,
|
||||
const std::string& master_server_entry, uint64_t local_buffer_size,
|
||||
const std::shared_ptr<TransferEngine>& transfer_engine,
|
||||
const std::string& ipc_socket_path, uint16_t metrics_port = 9003,
|
||||
bool enable_metrics_http = true,
|
||||
const std::map<std::string, std::string>& labels = {}) {
|
||||
// Parse local_hostname into IP and optional port.
|
||||
// Only set te_port when the user explicitly provides a port;
|
||||
// otherwise keep the default value (0 = randomly assigned).
|
||||
auto bracket_pos = local_hostname.find(']');
|
||||
if (bracket_pos != std::string::npos) {
|
||||
// Bracketed IPv6, e.g. "[2001:db8::1]" or "[2001:db8::1]:1234"
|
||||
config.local_ip = local_hostname.substr(1, bracket_pos - 1);
|
||||
auto colon_after = local_hostname.find(':', bracket_pos);
|
||||
if (colon_after != std::string::npos) {
|
||||
config.te_port = getPortFromString(
|
||||
local_hostname.substr(colon_after + 1), 0);
|
||||
}
|
||||
} else if (isValidIpV6(local_hostname)) {
|
||||
// Raw IPv6 without brackets, no way to specify port
|
||||
config.local_ip = local_hostname;
|
||||
} else {
|
||||
// IPv4 or hostname, optionally with port
|
||||
auto colon_pos = local_hostname.rfind(':');
|
||||
if (colon_pos != std::string::npos) {
|
||||
config.local_ip = local_hostname.substr(0, colon_pos);
|
||||
config.te_port =
|
||||
getPortFromString(local_hostname.substr(colon_pos + 1), 0);
|
||||
} else {
|
||||
config.local_ip = local_hostname;
|
||||
}
|
||||
}
|
||||
config.metadata_connstring = metadata_connstring;
|
||||
config.protocol = protocol;
|
||||
config.rdma_devices = rdma_devices;
|
||||
config.master_server_entry = master_server_entry;
|
||||
config.local_buffer_size = local_buffer_size;
|
||||
config.transfer_engine = transfer_engine;
|
||||
config.ipc_socket_path = ipc_socket_path;
|
||||
config.metrics_port = metrics_port;
|
||||
config.enable_metrics_http = enable_metrics_http;
|
||||
config.labels = labels;
|
||||
}
|
||||
|
||||
static LocalTransferMode parse_p2p_local_transfer_mode(std::string mode) {
|
||||
std::transform(
|
||||
mode.begin(), mode.end(), mode.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (mode == "memcpy") {
|
||||
return LocalTransferMode::MEMCPY;
|
||||
}
|
||||
if (mode == "te") {
|
||||
return LocalTransferMode::TE;
|
||||
}
|
||||
throw std::runtime_error(
|
||||
"Invalid p2p local transfer mode. Expected 'memcpy' or 'te'.");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <ylt/util/expected.hpp>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
|
||||
#include "heartbeat_type.h"
|
||||
#include "client_meta.h"
|
||||
#include "mutex.h"
|
||||
#include "rpc_types.h"
|
||||
#include "types.h"
|
||||
#include <random>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class ClientIterator {
|
||||
public:
|
||||
virtual ~ClientIterator() = default;
|
||||
|
||||
std::shared_ptr<ClientMeta> Next() {
|
||||
if (index_ < clients_.size()) {
|
||||
return clients_[index_++];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
ClientIterator() = default;
|
||||
|
||||
std::vector<std::shared_ptr<ClientMeta>> clients_;
|
||||
size_t index_ = 0;
|
||||
};
|
||||
|
||||
class OrderedClientIterator : public ClientIterator {
|
||||
public:
|
||||
OrderedClientIterator(
|
||||
const std::unordered_map<UUID, std::shared_ptr<ClientMeta>,
|
||||
boost::hash<UUID>>& client_metas) {
|
||||
clients_.reserve(client_metas.size());
|
||||
for (const auto& [id, meta] : client_metas) {
|
||||
clients_.emplace_back(meta);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class RandomClientIterator : public ClientIterator {
|
||||
public:
|
||||
RandomClientIterator(
|
||||
const std::unordered_map<UUID, std::shared_ptr<ClientMeta>,
|
||||
boost::hash<UUID>>& client_metas) {
|
||||
clients_.reserve(client_metas.size());
|
||||
for (const auto& [id, meta] : client_metas) {
|
||||
clients_.emplace_back(meta);
|
||||
}
|
||||
std::random_device rd;
|
||||
std::mt19937 g(rd());
|
||||
std::shuffle(clients_.begin(), clients_.end(), g);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief ClientManager is a base class for managing clients' lifecycle and
|
||||
* heartbeat with a three-state state machine (HEALTH/DISCONNECTION/CRASHED).
|
||||
*/
|
||||
class ClientManager {
|
||||
public:
|
||||
ClientManager(const int64_t disconnect_timeout_sec,
|
||||
const int64_t crash_timeout_sec,
|
||||
const ViewVersionId view_version);
|
||||
virtual ~ClientManager();
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
void StartClientMonitor();
|
||||
void StopClientMonitor();
|
||||
|
||||
/**
|
||||
* @brief Register a client with its segments.
|
||||
* Writes ClientMeta to client_metas_ and batch-mounts segments.
|
||||
* Must be called before any other client/segment operations.
|
||||
* @return RegisterClientResponse containing master's view_version
|
||||
*/
|
||||
auto RegisterClient(const RegisterClientRequest& req)
|
||||
-> tl::expected<RegisterClientResponse, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Process a heartbeat from a client.
|
||||
* 1. maintain client healthy status machine:
|
||||
* - If client not in client_metas_: returns UNDEFINED + view_version,
|
||||
* client should register it again.
|
||||
* - If CRASHED: returns CRASHED:
|
||||
* master is cleaning up the client meta, client should retry until cleaning
|
||||
* over and register it again
|
||||
* - If DISCONNECTION: recovers to HEALTH
|
||||
* 2. Processes lightweight sync tasks
|
||||
*/
|
||||
auto Heartbeat(const HeartbeatRequest& req)
|
||||
-> tl::expected<HeartbeatResponse, ErrorCode>;
|
||||
|
||||
auto QueryClientStatus(const QueryClientStatusRequest& req)
|
||||
-> tl::expected<QueryClientStatusResponse, ErrorCode>;
|
||||
|
||||
auto GetAllSegments() -> tl::expected<std::vector<std::string>, ErrorCode>;
|
||||
auto GetClientSegments(const UUID& client_id)
|
||||
-> tl::expected<std::vector<std::string>, ErrorCode>;
|
||||
|
||||
auto QuerySegments(const std::string& segment)
|
||||
-> tl::expected<std::pair<size_t, size_t>, ErrorCode>;
|
||||
|
||||
auto QuerySegment(const UUID& client_id, const UUID& segment_id)
|
||||
-> tl::expected<std::shared_ptr<Segment>, ErrorCode>;
|
||||
|
||||
auto QueryIp(const UUID& client_id)
|
||||
-> tl::expected<std::vector<std::string>, ErrorCode>;
|
||||
|
||||
auto GetClient(const UUID& client_id) -> std::shared_ptr<ClientMeta>;
|
||||
auto GetAllClients() -> std::vector<std::shared_ptr<ClientMeta>>;
|
||||
|
||||
/**
|
||||
* @brief Iterate clients in the order determined by strategy.
|
||||
* @param strategy Client iteration strategy
|
||||
* @param visitor Callback invoked for each client, return
|
||||
* <is_continue, error_reason>:
|
||||
* - if visitor occurs error, just return the `error_code`.
|
||||
* - otherwise, return bool value to indicate whether
|
||||
* the iteration is over.
|
||||
* @return if clients iteration correctly, just return nothing,
|
||||
* otherwise return the first non-OK ErrorCode from the visitor.
|
||||
*/
|
||||
using ClientVisitor = std::function<tl::expected<bool, ErrorCode>(
|
||||
const std::shared_ptr<ClientMeta>& client)>;
|
||||
auto ForEachClient(ObjectIterateStrategy strategy,
|
||||
const ClientVisitor& visitor)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
using SegmentRemovalCallback = std::function<void(const UUID& segment_id)>;
|
||||
void SetSegmentRemovalCallback(SegmentRemovalCallback cb);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Client monitor implementation with three-state machine.
|
||||
*/
|
||||
void ClientMonitorFunc();
|
||||
|
||||
/**
|
||||
* @brief simple heartbeat task dispatcher
|
||||
*/
|
||||
virtual HeartbeatTaskResult ProcessTask(const UUID& client_id,
|
||||
const HeartbeatTask& task) = 0;
|
||||
|
||||
virtual std::unique_ptr<ClientIterator> InnerBuildClientIterator(
|
||||
ObjectIterateStrategy strategy);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Create architecture-specific ClientMeta
|
||||
*/
|
||||
virtual std::shared_ptr<ClientMeta> CreateClientMeta(
|
||||
const RegisterClientRequest& req) = 0;
|
||||
|
||||
/**
|
||||
* @brief Hook called after a client is registered.
|
||||
* Subclasses can override to perform post-registration logic
|
||||
* (e.g., setting is_syncing flag for HA recovery).
|
||||
*/
|
||||
virtual void OnClientRegistered(
|
||||
const std::shared_ptr<ClientMeta>& /*meta*/) {}
|
||||
|
||||
/**
|
||||
* @brief Get the deployment mode of this ClientManager.
|
||||
* Used for architecture validation during client registration.
|
||||
*/
|
||||
virtual DeploymentMode GetDeploymentMode() const = 0;
|
||||
|
||||
protected:
|
||||
static constexpr uint64_t kClientMonitorSleepMs =
|
||||
1000; // 1000 ms sleep between client monitor checks
|
||||
|
||||
protected:
|
||||
mutable SharedMutex clients_mutex_;
|
||||
// Client metadata: client_id -> metadata (including health state)
|
||||
std::unordered_map<UUID, std::shared_ptr<ClientMeta>, boost::hash<UUID>>
|
||||
client_metas_ GUARDED_BY(clients_mutex_);
|
||||
std::thread client_monitor_thread_;
|
||||
std::atomic<bool> client_monitor_running_{false};
|
||||
const ViewVersionId view_version_; // Passed from MasterService
|
||||
SegmentRemovalCallback segment_removal_cb_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include "segment_manager.h"
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <ylt/util/expected.hpp>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
|
||||
#include "mutex.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
struct ClientHealthState {
|
||||
ClientStatus status = ClientStatus::UNDEFINED;
|
||||
std::chrono::steady_clock::time_point last_heartbeat;
|
||||
};
|
||||
/**
|
||||
* @brief ClientMeta records the meta data of a client including health status
|
||||
* and segment information.
|
||||
*/
|
||||
class ClientMeta {
|
||||
public:
|
||||
virtual ~ClientMeta() = default;
|
||||
|
||||
ClientMeta(const UUID& client_id);
|
||||
|
||||
tl::expected<void, ErrorCode> MountSegment(const Segment& segment);
|
||||
|
||||
tl::expected<void, ErrorCode> UnmountSegment(const UUID& segment_id);
|
||||
|
||||
tl::expected<std::vector<Segment>, ErrorCode> GetSegments();
|
||||
|
||||
tl::expected<std::pair<size_t, size_t>, ErrorCode> QuerySegments(
|
||||
const std::string& segment_name);
|
||||
tl::expected<std::shared_ptr<Segment>, ErrorCode> QuerySegment(
|
||||
const UUID& segment_id);
|
||||
|
||||
virtual tl::expected<std::vector<std::string>, ErrorCode> QueryIp(
|
||||
const UUID& client_id) = 0;
|
||||
|
||||
using SegmentRemovalCallback = std::function<void(const UUID& segment_id)>;
|
||||
void SetSegmentRemovalCallback(SegmentRemovalCallback cb);
|
||||
|
||||
public:
|
||||
static void SetTimeouts(int64_t disconnect_sec, int64_t crash_sec);
|
||||
|
||||
/**
|
||||
* @brief Update heartbeat timestamp and health status.
|
||||
* Attention: if client is CRASHED, the heartbeat will not be updated.
|
||||
* @return std::pair<ClientStatus, ClientStatus> {old_status, new_status}
|
||||
*/
|
||||
std::pair<ClientStatus, ClientStatus> Heartbeat();
|
||||
|
||||
/**
|
||||
* @brief Based on last heartbeat timestamp, update health status
|
||||
*
|
||||
* States machine:
|
||||
* - HEALTH:
|
||||
* -> DISCONNECTION: If (now - last_heartbeat) > disconnect_timeout_sec.
|
||||
*
|
||||
* - DISCONNECTION:
|
||||
* -> HEALTH: If (now - last_heartbeat) <= disconnect_timeout_sec.
|
||||
* (Implies a Heartbeat() call updated the timestamp).
|
||||
* -> CRASHED: If (now - last_heartbeat) > crash_timeout_sec.
|
||||
*
|
||||
* - CRASHED: Final state.
|
||||
*
|
||||
* @return std::pair<ClientStatus, ClientStatus> {old_status, new_status}
|
||||
*/
|
||||
std::pair<ClientStatus, ClientStatus> CheckHealth();
|
||||
|
||||
public:
|
||||
// Hooks for health status changes
|
||||
void OnDisconnected();
|
||||
virtual void DoOnDisconnected() = 0;
|
||||
void OnRecovered();
|
||||
virtual void DoOnRecovered() = 0;
|
||||
void OnCrashed();
|
||||
|
||||
public:
|
||||
UUID get_client_id() const { return client_id_; }
|
||||
|
||||
ClientHealthState get_health_state() const;
|
||||
bool is_health() const;
|
||||
|
||||
protected:
|
||||
tl::expected<void, ErrorCode> InnerStatusCheck() const;
|
||||
void InnerUpdateHeartbeat();
|
||||
std::pair<ClientStatus, ClientStatus> InnerUpdateHealthStatus();
|
||||
std::string HealthToString(ClientStatus status) const;
|
||||
|
||||
protected:
|
||||
virtual std::shared_ptr<SegmentManager> GetSegmentManager() = 0;
|
||||
|
||||
protected:
|
||||
static int64_t disconnect_timeout_sec_;
|
||||
static int64_t crash_timeout_sec_;
|
||||
|
||||
mutable SharedMutex client_mutex_;
|
||||
UUID client_id_;
|
||||
ClientHealthState health_state_ GUARDED_BY(client_mutex_);
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
#include "client_rpc_types.h"
|
||||
#include "data_manager.h"
|
||||
#include "types.h"
|
||||
#include <ylt/coro_rpc/coro_rpc_server.hpp>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @class ClientRpcService
|
||||
* @brief RPC service for handling remote data read/write requests from peer
|
||||
* clients
|
||||
*
|
||||
* This service receives RPC requests from other clients (e.g., Client A) to
|
||||
* read/write data stored locally (on Client B). It uses DataManager to access
|
||||
* TieredBackend and TransferEngine to perform zero-copy RDMA transfers.
|
||||
*/
|
||||
class ClientRpcService {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param data_manager Reference to DataManager instance (must outlive this
|
||||
* object)
|
||||
*/
|
||||
explicit ClientRpcService(DataManager& data_manager);
|
||||
|
||||
/**
|
||||
* @brief Read remote data: Client A requests Client B to read data and
|
||||
* transfer to A
|
||||
* @param request RemoteReadRequest containing key and destination buffers
|
||||
* @return ErrorCode indicating success or failure
|
||||
*
|
||||
* Flow:
|
||||
* 1. DataManager.ReadRemoteData(key, dest_buffers)
|
||||
* 2. TieredBackend.Get(key) → handle
|
||||
* 3. TransferEngine.submitTransfer(WRITE) to transfer data from B to A
|
||||
*/
|
||||
tl::expected<void, ErrorCode> ReadRemoteData(
|
||||
const RemoteReadRequest& request);
|
||||
|
||||
/**
|
||||
* @brief Write remote data: Client A requests Client B to write data from A
|
||||
* @param request RemoteWriteRequest containing key, source buffers, and
|
||||
* target_tier_id
|
||||
* @return UUID containing the route descriptor of the
|
||||
* written replica, or ErrorCode
|
||||
*/
|
||||
tl::expected<UUID, ErrorCode> WriteRemoteData(
|
||||
const RemoteWriteRequest& request);
|
||||
|
||||
private:
|
||||
DataManager& data_manager_; // Reference: owned by Client, same lifetime
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Register ClientRpcService methods with coro_rpc_server
|
||||
* @param server coro_rpc_server instance
|
||||
* @param service ClientRpcService instance
|
||||
*/
|
||||
void RegisterClientRpcService(coro_rpc::coro_rpc_server& server,
|
||||
ClientRpcService& service);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include "types.h"
|
||||
#include "ylt/struct_json/json_reader.h"
|
||||
#include "ylt/struct_json/json_writer.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @struct RemoteBufferDesc
|
||||
* @brief Describes a remote buffer location for RDMA transfer
|
||||
*/
|
||||
struct RemoteBufferDesc {
|
||||
std::string segment_endpoint; // Target segment endpoint
|
||||
uintptr_t addr; // Buffer address
|
||||
uint64_t size; // Buffer size in bytes
|
||||
};
|
||||
|
||||
YLT_REFL(RemoteBufferDesc, segment_endpoint, addr, size);
|
||||
|
||||
/**
|
||||
* @struct RemoteReadRequest
|
||||
* @brief RPC request for reading remote data
|
||||
*/
|
||||
struct RemoteReadRequest {
|
||||
std::string key; // Object key to read
|
||||
std::vector<RemoteBufferDesc>
|
||||
dest_buffers; // Destination buffers on remote client
|
||||
};
|
||||
|
||||
YLT_REFL(RemoteReadRequest, key, dest_buffers);
|
||||
|
||||
/**
|
||||
* @struct RemoteWriteRequest
|
||||
* @brief RPC request for writing remote data
|
||||
*/
|
||||
struct RemoteWriteRequest {
|
||||
std::string key;
|
||||
std::vector<RemoteBufferDesc> src_buffers;
|
||||
std::optional<UUID> target_tier_id;
|
||||
};
|
||||
|
||||
YLT_REFL(RemoteWriteRequest, key, src_buffers, target_tier_id);
|
||||
|
||||
/**
|
||||
* @struct BatchRemoteReadRequest
|
||||
* @brief Batch RPC request for reading multiple remote data objects
|
||||
*/
|
||||
struct BatchRemoteReadRequest {
|
||||
std::vector<std::string> keys; // Object keys to read
|
||||
std::vector<std::vector<RemoteBufferDesc>>
|
||||
dest_buffers_list; // Destination buffers for each key
|
||||
};
|
||||
|
||||
YLT_REFL(BatchRemoteReadRequest, keys, dest_buffers_list);
|
||||
|
||||
/**
|
||||
* @struct BatchRemoteWriteRequest
|
||||
* @brief Batch RPC request for writing multiple remote data objects
|
||||
*/
|
||||
struct BatchRemoteWriteRequest {
|
||||
std::vector<std::string> keys; // Object keys to write
|
||||
std::vector<std::vector<RemoteBufferDesc>>
|
||||
src_buffers_list; // Source buffers for each key
|
||||
std::vector<std::optional<UUID>>
|
||||
target_tier_ids; // Target tier IDs for each key
|
||||
};
|
||||
|
||||
YLT_REFL(BatchRemoteWriteRequest, keys, src_buffers_list, target_tier_ids);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -1,101 +1,92 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
#include <chrono>
|
||||
#include "mutex.h"
|
||||
|
||||
#include "client_metric.h"
|
||||
#include "ha_helper.h"
|
||||
#include "master_client.h"
|
||||
#include "storage_backend.h"
|
||||
#include "thread_pool.h"
|
||||
#include "transfer_engine.h"
|
||||
#include "transfer_task.h"
|
||||
#include "types.h"
|
||||
#include "p2p_rpc_types.h"
|
||||
#include "replica.h"
|
||||
#include "master_metric_manager.h"
|
||||
#include "master_client.h"
|
||||
#include <ylt/coro_rpc/coro_rpc_server.hpp>
|
||||
#include <ylt/coro_http/coro_http_server.hpp>
|
||||
#include "client_config_builder.h"
|
||||
#include "client_buffer.hpp"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class PutOperation;
|
||||
using WriteConfig = std::variant<ReplicateConfig, WriteRouteRequestConfig>;
|
||||
|
||||
/**
|
||||
* @brief Result of a query operation containing replica information and lease
|
||||
* timeout
|
||||
* @brief Result of a query operation containing replica information
|
||||
*/
|
||||
class QueryResult {
|
||||
public:
|
||||
/** @brief List of available replicas for the queried key */
|
||||
const std::vector<Replica::Descriptor> replicas;
|
||||
/** @brief Time point when the lease for this key expires */
|
||||
const std::chrono::steady_clock::time_point lease_timeout;
|
||||
|
||||
QueryResult(std::vector<Replica::Descriptor>&& replicas_param,
|
||||
std::chrono::steady_clock::time_point lease_timeout_param)
|
||||
: replicas(std::move(replicas_param)),
|
||||
lease_timeout(lease_timeout_param) {}
|
||||
explicit QueryResult(std::vector<Replica::Descriptor>&& replicas_param)
|
||||
: replicas(std::move(replicas_param)) {}
|
||||
|
||||
bool IsLeaseExpired() const {
|
||||
return std::chrono::steady_clock::now() >= lease_timeout;
|
||||
}
|
||||
virtual ~QueryResult() = default;
|
||||
|
||||
bool IsLeaseExpired(std::chrono::steady_clock::time_point& now) const {
|
||||
return now >= lease_timeout;
|
||||
}
|
||||
// Disable copy to prevent slicing; allow move
|
||||
QueryResult(const QueryResult&) = delete;
|
||||
QueryResult& operator=(const QueryResult&) = delete;
|
||||
QueryResult(QueryResult&&) = default;
|
||||
QueryResult& operator=(QueryResult&&) = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Client for interacting with the mooncake distributed object store
|
||||
*/
|
||||
class Client {
|
||||
class ClientService {
|
||||
public:
|
||||
~Client();
|
||||
virtual ~ClientService();
|
||||
|
||||
/**
|
||||
* @brief Creates and initializes a new Client instance
|
||||
* @param local_hostname Local host address (IP:Port)
|
||||
* @param metadata_connstring Connection string for metadata service
|
||||
* @param protocol Transfer protocol ("rdma" or "tcp")
|
||||
* @param device_names Comma-separated RDMA device names.
|
||||
* Optional with default auto-discovery. Only required when
|
||||
* auto-discovery is disabled (set env `MC_MS_AUTO_DISC=0`).
|
||||
* @param master_server_entry The entry of master server (IP:Port of master
|
||||
* address for non-HA mode, etcd://IP:Port;IP:Port;...;IP:Port for
|
||||
* HA mode)
|
||||
* @return std::optional containing a shared_ptr to Client if successful,
|
||||
* std::nullopt otherwise
|
||||
* @brief stops background threads
|
||||
*/
|
||||
static std::optional<std::shared_ptr<Client>> Create(
|
||||
const std::string& local_hostname,
|
||||
const std::string& metadata_connstring, const std::string& protocol,
|
||||
const std::optional<std::string>& device_names = std::nullopt,
|
||||
const std::string& master_server_entry = kDefaultMasterAddress,
|
||||
const std::shared_ptr<TransferEngine>& transfer_engine = nullptr,
|
||||
std::map<std::string, std::string> labels = {});
|
||||
virtual void Stop();
|
||||
|
||||
/**
|
||||
* @brief Retrieves data for a given key
|
||||
* @param object_key Key to retrieve
|
||||
* @param slices Vector of slices to store the retrieved data
|
||||
* @return ErrorCode indicating success/failure
|
||||
* @brief stops heartbeat thread
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Get(const std::string& object_key,
|
||||
std::vector<Slice>& slices);
|
||||
virtual void StopHeartbeat();
|
||||
|
||||
/**
|
||||
* @brief Batch retrieve data for multiple keys
|
||||
* @param object_keys Keys to query
|
||||
* @param slices Map of object keys to their data slices
|
||||
* @brief Release internal resources. Should be called after Stop()
|
||||
*/
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchGet(
|
||||
const std::vector<std::string>& object_keys,
|
||||
std::unordered_map<std::string, std::vector<Slice>>& slices);
|
||||
virtual void Destroy();
|
||||
|
||||
/**
|
||||
* @brief Creates and initializes a new ClientService instance
|
||||
* @param config The start up configuration for the client service.
|
||||
* @return std::optional containing a shared_ptr to ClientService if
|
||||
* successful, std::nullopt otherwise
|
||||
*/
|
||||
static std::optional<std::shared_ptr<ClientService>> Create(
|
||||
const CentralizedClientConfig& config);
|
||||
static std::optional<std::shared_ptr<ClientService>> Create(
|
||||
const P2PClientConfig& config);
|
||||
|
||||
/**
|
||||
* @brief Returns the deployment mode of the client service.
|
||||
* @return DeploymentMode (CENTRALIZATION or P2P).
|
||||
*/
|
||||
virtual DeploymentMode deployment_mode() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Batch query IP addresses for multiple client IDs.
|
||||
|
|
@ -103,76 +94,89 @@ class Client {
|
|||
* @return An expected object containing a map from client_id to their IP
|
||||
* address lists on success, or an ErrorCode on failure.
|
||||
*/
|
||||
tl::expected<
|
||||
virtual tl::expected<
|
||||
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
|
||||
ErrorCode>
|
||||
BatchQueryIp(const std::vector<UUID>& client_ids);
|
||||
|
||||
/**
|
||||
* @brief Gets object metadata without transferring data
|
||||
* @param object_key Key to query
|
||||
* @return QueryResult containing replicas and lease timeout, or ErrorCode
|
||||
* indicating failure
|
||||
*/
|
||||
tl::expected<QueryResult, ErrorCode> Query(const std::string& object_key);
|
||||
|
||||
/**
|
||||
* @brief Queries replica lists for object keys that match a regex pattern.
|
||||
* @param str The regular expression string to match against object keys.
|
||||
* @return An expected object containing a map from object keys to their
|
||||
* replica descriptors on success, or an ErrorCode on failure.
|
||||
*/
|
||||
tl::expected<
|
||||
virtual tl::expected<
|
||||
std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
|
||||
ErrorCode>
|
||||
QueryByRegex(const std::string& str);
|
||||
|
||||
/**
|
||||
* @brief Gets object metadata without transferring data
|
||||
* @param object_key Key to query
|
||||
* @return QueryResult (or its subclass) containing replicas, or ErrorCode
|
||||
* indicating failure
|
||||
*/
|
||||
virtual tl::expected<std::unique_ptr<QueryResult>, ErrorCode> Query(
|
||||
const std::string& object_key, const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
/**
|
||||
* @brief Batch query object metadata without transferring data
|
||||
* @param object_keys Keys to query
|
||||
* @return Vector of QueryResult objects containing replicas and lease
|
||||
* timeouts
|
||||
* @return Vector of QueryResult (or its subclass) containing replicas
|
||||
*/
|
||||
std::vector<tl::expected<QueryResult, ErrorCode>> BatchQuery(
|
||||
const std::vector<std::string>& object_keys);
|
||||
virtual std::vector<tl::expected<std::unique_ptr<QueryResult>, ErrorCode>>
|
||||
BatchQuery(const std::vector<std::string>& object_keys,
|
||||
const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
/**
|
||||
* @brief Batch clear KV cache for specified object keys on a specific
|
||||
* segment for a given client.
|
||||
* @param object_keys Vector of object key strings to clear.
|
||||
* @param client_id The UUID of the client that owns the object keys.
|
||||
* @param segment_name The name of the segment (storage device) to clear
|
||||
* from.
|
||||
* @return An expected object containing a vector of successfully cleared
|
||||
* object keys on success, or an ErrorCode on failure.
|
||||
* @brief Gets data with memory allocation
|
||||
* @param key Object key
|
||||
* @param allocator Read buffer allocator
|
||||
* @param config Read route config
|
||||
* @return BufferHandle allocated by `allocator` on success.
|
||||
* ErrorCode on failure.
|
||||
*/
|
||||
tl::expected<std::vector<std::string>, ErrorCode> BatchReplicaClear(
|
||||
const std::vector<std::string>& object_keys, const UUID& client_id,
|
||||
const std::string& segment_name);
|
||||
virtual tl::expected<std::shared_ptr<BufferHandle>, ErrorCode> Get(
|
||||
const std::string& key,
|
||||
std::shared_ptr<ClientBufferAllocator> allocator,
|
||||
const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
virtual std::vector<tl::expected<std::shared_ptr<BufferHandle>, ErrorCode>>
|
||||
BatchGet(const std::vector<std::string>& keys,
|
||||
std::shared_ptr<ClientBufferAllocator> allocator,
|
||||
const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
/**
|
||||
* @brief Transfers data using pre-queried object information
|
||||
* @param object_key Key of the object
|
||||
* @param query_result Previously queried object metadata containing
|
||||
* replicas and lease timeout
|
||||
* @param slices Vector of slices to store the data
|
||||
* @return ErrorCode indicating success/failure
|
||||
* @brief Gets data into user-provided buffers without memory allocation
|
||||
* @param key Object key
|
||||
* @param buffers Vector of destination buffer pointers
|
||||
* @param sizes Vector of buffer sizes (must match buffers.size())
|
||||
* @param config Read route config
|
||||
* @return Number of bytes read on success. ErrorCode on failure.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Get(const std::string& object_key,
|
||||
const QueryResult& query_result,
|
||||
std::vector<Slice>& slices);
|
||||
virtual tl::expected<int64_t, ErrorCode> Get(
|
||||
const std::string& key, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
/**
|
||||
* @brief Transfers data using pre-queried object information
|
||||
* @param object_keys Keys of the objects
|
||||
* @param query_results Previously queried object metadata for each key
|
||||
* @param slices Map of object keys to their data slices
|
||||
* @return Vector of ErrorCode results for each object
|
||||
* @brief Batch get data into user-provided buffers
|
||||
* @param keys Object keys
|
||||
* @param all_buffers Vector of buffer pointer vectors (one per key)
|
||||
* @param all_sizes Vector of buffer size vectors (one per key)
|
||||
* @param config Read route config
|
||||
* @param aggregate_same_segment_task
|
||||
* Whether to aggregate read tasks on the same segment.
|
||||
* If false, each key will be generated as a independent task.
|
||||
* Otherwise, the tasks will be aggregated on the same segment.
|
||||
* @return Vector of bytes read on success. ErrorCode on failure.
|
||||
*/
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchGet(
|
||||
const std::vector<std::string>& object_keys,
|
||||
const std::vector<QueryResult>& query_results,
|
||||
std::unordered_map<std::string, std::vector<Slice>>& slices,
|
||||
bool prefer_same_node = false);
|
||||
virtual std::vector<tl::expected<int64_t, ErrorCode>> BatchGet(
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
const ReadRouteConfig& config = {},
|
||||
bool aggregate_same_segment_task = false) = 0;
|
||||
|
||||
/**
|
||||
* @brief Stores data with replication
|
||||
|
|
@ -181,9 +185,9 @@ class Client {
|
|||
* @param config Replication configuration
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Put(const ObjectKey& key,
|
||||
std::vector<Slice>& slices,
|
||||
const ReplicateConfig& config);
|
||||
virtual tl::expected<void, ErrorCode> Put(const ObjectKey& key,
|
||||
std::vector<Slice>& slices,
|
||||
const WriteConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief Batch put data with replication
|
||||
|
|
@ -192,17 +196,17 @@ class Client {
|
|||
* to match keys)
|
||||
* @param config Replication configuration
|
||||
*/
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPut(
|
||||
virtual std::vector<tl::expected<void, ErrorCode>> BatchPut(
|
||||
const std::vector<ObjectKey>& keys,
|
||||
std::vector<std::vector<Slice>>& batched_slices,
|
||||
const ReplicateConfig& config);
|
||||
const WriteConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief Removes an object and all its replicas
|
||||
* @param key Key to remove
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Remove(const ObjectKey& key);
|
||||
virtual tl::expected<void, ErrorCode> Remove(const ObjectKey& key) = 0;
|
||||
|
||||
/**
|
||||
* @brief Removes objects from the store whose keys match a regex pattern.
|
||||
|
|
@ -210,13 +214,14 @@ class Client {
|
|||
* @return An expected object containing the number of removed objects on
|
||||
* success, or an ErrorCode on failure.
|
||||
*/
|
||||
tl::expected<long, ErrorCode> RemoveByRegex(const ObjectKey& str);
|
||||
virtual tl::expected<long, ErrorCode> RemoveByRegex(
|
||||
const ObjectKey& str) = 0;
|
||||
|
||||
/**
|
||||
* @brief Removes all objects and all its replicas
|
||||
* @return tl::expected<long, ErrorCode> number of removed objects or error
|
||||
*/
|
||||
tl::expected<long, ErrorCode> RemoveAll();
|
||||
virtual tl::expected<long, ErrorCode> RemoveAll() = 0;
|
||||
|
||||
/**
|
||||
* @brief Registers a memory segment to master for allocation
|
||||
|
|
@ -224,7 +229,8 @@ class Client {
|
|||
* @param size Size of the buffer in bytes
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> MountSegment(const void* buffer, size_t size);
|
||||
virtual tl::expected<void, ErrorCode> MountSegment(const void* buffer,
|
||||
size_t size) = 0;
|
||||
|
||||
/**
|
||||
* @brief Unregisters a memory segment from master
|
||||
|
|
@ -232,8 +238,8 @@ class Client {
|
|||
* @param size Size of the buffer in bytes
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> UnmountSegment(const void* buffer,
|
||||
size_t size);
|
||||
virtual tl::expected<void, ErrorCode> UnmountSegment(const void* buffer,
|
||||
size_t size) = 0;
|
||||
|
||||
/**
|
||||
* @brief Registers memory buffer with TransferEngine for data transfer
|
||||
|
|
@ -260,65 +266,17 @@ class Client {
|
|||
/**
|
||||
* @brief Checks if an object exists
|
||||
* @param key Key to check
|
||||
* @return ErrorCode::OK if exists, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* exists, other ErrorCode for errors
|
||||
* @return True if exists, false if not, or ErrorCode for unexpected errors.
|
||||
*/
|
||||
tl::expected<bool, ErrorCode> IsExist(const std::string& key);
|
||||
virtual tl::expected<bool, ErrorCode> IsExist(const std::string& key) = 0;
|
||||
|
||||
/**
|
||||
* @brief Checks if multiple objects exist
|
||||
* @param keys Vector of keys to check
|
||||
* @return Vector of existence results for each key
|
||||
*/
|
||||
std::vector<tl::expected<bool, ErrorCode>> BatchIsExist(
|
||||
const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief Mounts a local disk segment into the master.
|
||||
* @param enable_offloading If true, enables offloading (write-to-file).
|
||||
*/
|
||||
tl::expected<void, ErrorCode> MountLocalDiskSegment(bool enable_offloading);
|
||||
|
||||
/**
|
||||
* @brief Heartbeat call to collect object-level statistics and retrieve the
|
||||
* set of non-offloaded objects.
|
||||
* @param enable_offloading Indicates whether offloading is enabled for this
|
||||
* segment.
|
||||
* @param offloading_objects On return, contains a map from object key to
|
||||
* size (in bytes) for all objects that require offload.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> OffloadObjectHeartbeat(
|
||||
bool enable_offloading,
|
||||
std::unordered_map<std::string, int64_t>& offloading_objects);
|
||||
|
||||
/**
|
||||
* @brief Performs a batched write of multiple objects using a
|
||||
* high-throughput Transfer Engine.
|
||||
* @param transfer_engine_addr Address of the Transfer Engine service (e.g.,
|
||||
* "ip:port").
|
||||
* @param keys List of keys identifying the data objects to be transferred
|
||||
* @param pointers Array of destination memory addresses on the remote node
|
||||
* where data will be written (one per key)
|
||||
* @param batched_slices Map from object key to its data slice
|
||||
* (`mooncake::Slice`), containing raw bytes to be written.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> BatchPutOffloadObject(
|
||||
const std::string& transfer_engine_addr,
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<uintptr_t>& pointers,
|
||||
const std::unordered_map<std::string, Slice>& batched_slices);
|
||||
|
||||
/**
|
||||
* @brief Notifies the master that offloading of specified objects has
|
||||
* succeeded.
|
||||
* @param keys A list of object keys (names) that were successfully
|
||||
* offloaded.
|
||||
* @param metadatas The corresponding metadata for each offloaded object,
|
||||
* including size, storage location, etc.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> NotifyOffloadSuccess(
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas);
|
||||
virtual std::vector<tl::expected<bool, ErrorCode>> BatchIsExist(
|
||||
const std::vector<std::string>& keys) = 0;
|
||||
|
||||
// For human-readable metrics
|
||||
tl::expected<std::string, ErrorCode> GetSummaryMetrics() {
|
||||
|
|
@ -330,7 +288,12 @@ class Client {
|
|||
|
||||
tl::expected<MasterMetricManager::CacheHitStatDict, ErrorCode>
|
||||
CalcCacheStats() {
|
||||
return master_client_.CalcCacheStats();
|
||||
auto guard = AcquireInflightGuard();
|
||||
if (!guard.is_valid()) {
|
||||
LOG(ERROR) << "client is shutting down";
|
||||
return tl::unexpected(ErrorCode::SHUTTING_DOWN);
|
||||
}
|
||||
return GetMasterClient().CalcCacheStats();
|
||||
}
|
||||
|
||||
// For Prometheus-style metrics
|
||||
|
|
@ -343,79 +306,223 @@ class Client {
|
|||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the metrics HTTP server port.
|
||||
* @return The port number, or 0 if metrics server is disabled.
|
||||
*/
|
||||
uint16_t GetMetricsPort() const { return metrics_port_; }
|
||||
|
||||
/**
|
||||
* @brief Checks if metrics HTTP server is enabled.
|
||||
* @return True if enabled, false otherwise.
|
||||
*/
|
||||
bool IsMetricsHttpEnabled() const { return enable_metrics_http_; }
|
||||
|
||||
/**
|
||||
* @brief Gets the health status for the /health endpoint.
|
||||
* @return A string representing the health status.
|
||||
*/
|
||||
virtual std::string GetHealthStatus() const { return "OK"; }
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Gets the local transport endpoint (IP and port).
|
||||
* @return The transport endpoint string.
|
||||
*/
|
||||
[[nodiscard]] std::string GetTransportEndpoint() {
|
||||
return transfer_engine_->getLocalIpAndPort();
|
||||
}
|
||||
UUID GetClientID() const { return client_id_; }
|
||||
ViewVersionId GetViewVersion() const { return view_version_.load(); }
|
||||
|
||||
private:
|
||||
public:
|
||||
/**
|
||||
* @brief Checks if memory registration parameters are valid
|
||||
* @param addr Memory address to check
|
||||
* @param length Size of the memory region
|
||||
* @return ErrorCode indicating success or failure
|
||||
*/
|
||||
static tl::expected<void, ErrorCode> CheckRegisterMemoryParams(
|
||||
const void* addr, size_t length);
|
||||
|
||||
/**
|
||||
* @brief Calculate the total size of a list of slices.
|
||||
* @param slices Vector of slices.
|
||||
* @return Total size in bytes.
|
||||
*/
|
||||
[[nodiscard]] static size_t CalculateSliceSize(
|
||||
const std::vector<Slice>& slices);
|
||||
|
||||
/**
|
||||
* @brief Calculate the total size of a list of slices.
|
||||
* @param slices Span of slices.
|
||||
* @return Total size in bytes.
|
||||
*/
|
||||
[[nodiscard]] static size_t CalculateSliceSize(
|
||||
std::span<const Slice> slices);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Private constructor to enforce creation through Create() method
|
||||
*/
|
||||
Client(const std::string& local_hostname,
|
||||
const std::string& metadata_connstring,
|
||||
const std::map<std::string, std::string>& labels = {});
|
||||
ClientService(const std::string& local_ip, uint16_t te_port,
|
||||
const std::string& metadata_connstring,
|
||||
uint16_t metrics_port = 9003, bool enable_metrics_http = true,
|
||||
const std::map<std::string, std::string>& labels = {});
|
||||
|
||||
/**
|
||||
* @brief Internal helper functions for initialization and data transfer
|
||||
* @brief Get the RPC Client for Master service calls
|
||||
* @return Reference to MasterClient
|
||||
*/
|
||||
virtual MasterClient& GetMasterClient() = 0;
|
||||
|
||||
/**
|
||||
* @brief Connects to the master server.
|
||||
* @param master_server_entry Entry point of the master server.
|
||||
* @return ErrorCode indicating success or failure.
|
||||
*/
|
||||
ErrorCode ConnectToMaster(const std::string& master_server_entry);
|
||||
|
||||
/**
|
||||
* @brief Initializes the Transfer Engine.
|
||||
* @param local_hostname Local hostname or IP.
|
||||
* @param metadata_connstring Connection string for metadata service.
|
||||
* @param protocol Transport protocol (e.g., "tcp", "rdma").
|
||||
* @param device_names Optional RDMA device names.
|
||||
* @return ErrorCode indicating success or failure.
|
||||
*/
|
||||
ErrorCode InitTransferEngine(
|
||||
const std::string& local_hostname,
|
||||
const std::string& metadata_connstring, const std::string& protocol,
|
||||
const std::string& endpoint, const std::string& metadata_connstring,
|
||||
const std::string& protocol,
|
||||
const std::optional<std::string>& device_names);
|
||||
void InitTransferSubmitter();
|
||||
ErrorCode TransferData(const Replica::Descriptor& replica_descriptor,
|
||||
std::vector<Slice>& slices,
|
||||
TransferRequest::OpCode op_code);
|
||||
ErrorCode TransferWrite(const Replica::Descriptor& replica_descriptor,
|
||||
std::vector<Slice>& slices);
|
||||
ErrorCode TransferRead(const Replica::Descriptor& replica_descriptor,
|
||||
std::vector<Slice>& slices);
|
||||
|
||||
protected:
|
||||
// Heartbeat-related function
|
||||
|
||||
/**
|
||||
* @brief Prepare and use the storage backend for persisting data
|
||||
* @brief Starts the heartbeat thread.
|
||||
* @param master_server_entry Entry point of the master server.
|
||||
*/
|
||||
void PrepareStorageBackend(const std::string& storage_root_dir,
|
||||
const std::string& fsdir,
|
||||
bool enable_eviction = true,
|
||||
uint64_t quota_bytes = 0);
|
||||
void StartHeartbeat(const std::string& master_server_entry);
|
||||
|
||||
void PutToLocalFile(const std::string& object_key,
|
||||
const std::vector<Slice>& slices,
|
||||
const DiskDescriptor& disk_descriptor);
|
||||
void HeartbeatThreadMain(bool is_ha_mode,
|
||||
std::string current_master_address,
|
||||
const std::string& master_server_entry);
|
||||
|
||||
/**
|
||||
* @brief Find the first complete replica from a replica list
|
||||
* @param replica_list List of replicas to search through
|
||||
* @param replica the first complete replica (file or memory)
|
||||
* @return ErrorCode::OK if found, ErrorCode::INVALID_REPLICA if no complete
|
||||
* replica
|
||||
* @brief Handles a successful heartbeat response.
|
||||
* Triggers async RegisterClient if master reports UNDEFINED status.
|
||||
* Fires MASTER_RECONNECTED if connection_interrupted_ was set.
|
||||
* @return true if heartbeat was successfully processed.
|
||||
*/
|
||||
ErrorCode FindFirstCompleteReplica(
|
||||
const std::vector<Replica::Descriptor>& replica_list,
|
||||
Replica::Descriptor& replica);
|
||||
bool HandleHeartbeatResponse(const HeartbeatResponse& response,
|
||||
const std::string& current_master_address,
|
||||
const std::function<void()>& register_client,
|
||||
std::future<void>& register_client_future);
|
||||
|
||||
/**
|
||||
* @brief Batch put helper methods for structured approach
|
||||
* @brief Handles the result of a task received in a heartbeat response.
|
||||
* @param task_result The result of the task.
|
||||
*/
|
||||
std::vector<PutOperation> CreatePutOperations(
|
||||
const std::vector<ObjectKey>& keys,
|
||||
const std::vector<std::vector<Slice>>& batched_slices);
|
||||
void StartBatchPut(std::vector<PutOperation>& ops,
|
||||
const ReplicateConfig& config);
|
||||
void SubmitTransfers(std::vector<PutOperation>& ops);
|
||||
void WaitForTransfers(std::vector<PutOperation>& ops);
|
||||
void FinalizeBatchPut(std::vector<PutOperation>& ops);
|
||||
std::vector<tl::expected<void, ErrorCode>> CollectResults(
|
||||
const std::vector<PutOperation>& ops);
|
||||
void HandleHeartbeatTaskResult(const HeartbeatTaskResult& task_result);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPutWhenPreferSameNode(
|
||||
std::vector<PutOperation>& ops);
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchGetWhenPreferSameNode(
|
||||
const std::vector<std::string>& object_keys,
|
||||
const std::vector<QueryResult>& query_results,
|
||||
std::unordered_map<std::string, std::vector<Slice>>& slices);
|
||||
/**
|
||||
* @brief Attempts to reconnect to master after heartbeat failures.
|
||||
* For HA mode, fetches the latest master address from etcd.
|
||||
* For non-HA mode, reconnects to the current_master_address.
|
||||
* @param is_ha_mode Whether HA mode is enabled.
|
||||
* @param current_master_address Current master address, may be updated
|
||||
* after successful reconnection in HA mode.
|
||||
* @return true if reconnect succeeded, false otherwise.
|
||||
*/
|
||||
bool ReconnectToMaster(bool is_ha_mode,
|
||||
std::string& current_master_address);
|
||||
|
||||
/**
|
||||
* @brief Waits for the next heartbeat interval using condition variable.
|
||||
* @param interval_ms Milliseconds to wait.
|
||||
*/
|
||||
void WaitForNextHeartbeat(int interval_ms);
|
||||
virtual HeartbeatRequest build_heartbeat_request() = 0;
|
||||
|
||||
/**
|
||||
* @brief Starts the metrics HTTP server.
|
||||
* @param enable_metrics_http Whether to enable the HTTP server.
|
||||
* @param metrics_port Port to use.
|
||||
* @return The actual port number, or 0 if disabled.
|
||||
*/
|
||||
uint16_t StartMetricsHttpServer(bool enable_metrics_http,
|
||||
uint16_t metrics_port);
|
||||
|
||||
/**
|
||||
* @brief Stops the metrics HTTP server.
|
||||
*/
|
||||
void StopMetricsHttpServer();
|
||||
|
||||
/**
|
||||
* @brief Registers the client into the master server.
|
||||
* @return An ErrorCode indicating success or failure.
|
||||
*/
|
||||
virtual tl::expected<RegisterClientResponse, ErrorCode>
|
||||
RegisterClient() = 0;
|
||||
|
||||
/**
|
||||
* @brief Single hook for all HA-related events from the heartbeat loop.
|
||||
* Subclasses override to handle state transitions.
|
||||
*/
|
||||
virtual void OnHAEvent(HAEvent event) { (void)event; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief RAII guard for managing in-flight requests during service
|
||||
* shutdown.
|
||||
*/
|
||||
class InflightRequestGuard {
|
||||
public:
|
||||
explicit InflightRequestGuard(ClientService* client)
|
||||
: client_(client),
|
||||
valid_(false),
|
||||
lock_(&client_->running_rw_mtx_, shared_lock) {
|
||||
valid_ = client_->is_running_;
|
||||
}
|
||||
~InflightRequestGuard() = default;
|
||||
|
||||
InflightRequestGuard(const InflightRequestGuard&) = delete;
|
||||
InflightRequestGuard& operator=(const InflightRequestGuard&) = delete;
|
||||
InflightRequestGuard(InflightRequestGuard&& other) = delete;
|
||||
InflightRequestGuard& operator=(InflightRequestGuard&& other) = delete;
|
||||
|
||||
bool is_valid() const { return valid_; }
|
||||
|
||||
private:
|
||||
ClientService* client_;
|
||||
bool valid_;
|
||||
SharedMutexLocker lock_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Acquires an inflight request guard.
|
||||
* @return An InflightRequestGuard. If shutting down, is_valid() will be
|
||||
* false.
|
||||
*/
|
||||
InflightRequestGuard AcquireInflightGuard() {
|
||||
return InflightRequestGuard(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Marks the service as shutting down.
|
||||
* @return true if successfully marked, false if already shutting down.
|
||||
*/
|
||||
bool MarkShuttingDown() {
|
||||
SharedMutexLocker lock(&running_rw_mtx_);
|
||||
if (!is_running_) return false;
|
||||
is_running_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
friend class InflightRequestGuard;
|
||||
|
||||
protected:
|
||||
// Client identification
|
||||
const UUID client_id_;
|
||||
|
||||
|
|
@ -424,26 +531,61 @@ class Client {
|
|||
|
||||
// Core components
|
||||
std::shared_ptr<TransferEngine> transfer_engine_;
|
||||
MasterClient master_client_;
|
||||
std::unique_ptr<TransferSubmitter> transfer_submitter_;
|
||||
// Global segment pointers
|
||||
struct SegmentDeleter {
|
||||
void operator()(void* ptr) {
|
||||
if (ptr) {
|
||||
free(ptr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Mutex to protect mounted_segments_
|
||||
std::mutex mounted_segments_mutex_;
|
||||
std::unordered_map<UUID, Segment, boost::hash<UUID>> mounted_segments_;
|
||||
struct AscendSegmentDeleter {
|
||||
void operator()(void* ptr) {
|
||||
if (ptr) {
|
||||
free_memory("ascend", ptr);
|
||||
}
|
||||
}
|
||||
};
|
||||
std::vector<std::unique_ptr<void, SegmentDeleter>> segment_ptrs_;
|
||||
std::vector<std::unique_ptr<void, AscendSegmentDeleter>>
|
||||
ascend_segment_ptrs_;
|
||||
|
||||
// Configuration
|
||||
const std::string local_hostname_;
|
||||
const std::string local_ip_;
|
||||
const uint16_t te_port_;
|
||||
std::string local_endpoint() const {
|
||||
return local_ip_ + ":" + std::to_string(te_port_);
|
||||
}
|
||||
|
||||
// The segment endpoint that the transfer engine registered with the
|
||||
// metadata backend.
|
||||
std::string te_endpoint_;
|
||||
void initTeEndpoint();
|
||||
const std::string& get_te_endpoint() const { return te_endpoint_; }
|
||||
|
||||
const std::string metadata_connstring_;
|
||||
|
||||
// Client persistent thread pool for async operations
|
||||
ThreadPool write_thread_pool_;
|
||||
std::shared_ptr<StorageBackend> storage_backend_;
|
||||
|
||||
// For high availability
|
||||
MasterViewHelper master_view_helper_;
|
||||
std::thread ping_thread_;
|
||||
std::atomic<bool> ping_running_{false};
|
||||
void PingThreadMain(bool is_ha_mode, std::string current_master_address);
|
||||
std::thread heartbeat_thread_;
|
||||
std::atomic<bool> heartbeat_running_{false};
|
||||
std::condition_variable heartbeat_cv_;
|
||||
std::mutex heartbeat_mtx_;
|
||||
/// View version from master. Updated by async registration thread,
|
||||
/// read by heartbeat thread.
|
||||
std::atomic<ViewVersionId> view_version_{0};
|
||||
/// True after MASTER_UNREACHABLE fires; cleared when MASTER_RECONNECTED
|
||||
/// fires. Only accessed from the heartbeat thread — no locking required.
|
||||
bool connection_interrupted_ = false;
|
||||
|
||||
// Shutdown protection
|
||||
SharedMutex running_rw_mtx_;
|
||||
bool is_running_ GUARDED_BY(running_rw_mtx_) = false;
|
||||
|
||||
// Metrics HTTP server
|
||||
std::unique_ptr<coro_http::coro_http_server> metrics_http_server_;
|
||||
uint16_t metrics_port_ = 0; // 0 means disabled
|
||||
bool enable_metrics_http_ = true;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
|
|||
|
|
@ -0,0 +1,386 @@
|
|||
#pragma once
|
||||
|
||||
#include <shared_mutex>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
#include "async_memcpy_executor.h"
|
||||
#include "client_buffer.hpp"
|
||||
#include "client_config_builder.h"
|
||||
#include "task_handle.h"
|
||||
#include "tiered_cache/tiered_backend.h"
|
||||
#include "transfer_engine.h"
|
||||
#include "types.h"
|
||||
#include "client_rpc_types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @struct ReadTaskHandle
|
||||
* @brief Handle for a read operation
|
||||
*/
|
||||
struct ReadTaskHandle {
|
||||
std::unique_ptr<TaskHandle<void>> task_handle;
|
||||
int64_t data_size;
|
||||
|
||||
// if user use zero-copy get(), the var is useless;
|
||||
// if user provides allocator, the var is the buffer allocated by allocator;
|
||||
std::shared_ptr<BufferHandle> read_buf;
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct LocalTransferConfig
|
||||
* @brief Configuration for local data transfer operations
|
||||
*/
|
||||
struct LocalTransferConfig {
|
||||
LocalTransferMode mode = LocalTransferMode::TE;
|
||||
|
||||
// When mode == TE, the following parameters are used:
|
||||
std::string te_endpoint;
|
||||
|
||||
// When mode == MEMCPY, the following parameters are used:
|
||||
// 0 means forbid async memcpy (fall back to synchronous).
|
||||
size_t local_memcpy_async_worker_num = 32;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class DataManager
|
||||
* @brief Manages data access operations using TieredBackend and TransferEngine
|
||||
*
|
||||
* Provides unified interface for local and remote data operations, handling
|
||||
* tiered storage access and zero-copy transfers.
|
||||
*/
|
||||
class DataManager {
|
||||
// Allow test class to access private methods for testing
|
||||
friend class DataManagerTest;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param tiered_backend Unique pointer to TieredBackend instance (takes
|
||||
* ownership)
|
||||
* @param transfer_engine Shared pointer to TransferEngine instance (shared
|
||||
* with Client)
|
||||
*/
|
||||
DataManager(std::unique_ptr<TieredBackend> tiered_backend,
|
||||
std::shared_ptr<TransferEngine> transfer_engine,
|
||||
size_t lock_shard_count = 1024,
|
||||
const LocalTransferConfig& local_transfer_config = {});
|
||||
|
||||
void Stop() {
|
||||
if (async_memcpy_executor_) {
|
||||
async_memcpy_executor_->Shutdown();
|
||||
}
|
||||
if (tiered_backend_) {
|
||||
tiered_backend_->Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cleanup: delegates to TieredBackend::Destroy().
|
||||
*/
|
||||
void Destroy() {
|
||||
if (tiered_backend_) {
|
||||
tiered_backend_->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Public local read/write interface
|
||||
// Internally selects TE or Memcpy path based on config.
|
||||
// ================================================================
|
||||
|
||||
// The Put operation consists of three phases:
|
||||
// 1. Allocation: allocate memory from the tiered backend for the data
|
||||
// 2. Write: write the data to the allocated memory
|
||||
// 3. Commit: commit the data to the tiered backend
|
||||
//
|
||||
// IMPORTANT: The caller must keep the memory referenced by `slices` alive
|
||||
// from the time Put() returns until TaskHandle::Wait() completes. The
|
||||
// returned TaskHandle may capture raw pointers from the slices for
|
||||
// asynchronous transfer.
|
||||
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode> Put(
|
||||
const std::string& key, std::vector<Slice>& slices);
|
||||
|
||||
// Attention!!!
|
||||
// Get() method run without key lock.
|
||||
// It works based on two assumptions:
|
||||
// 1. We assume that each key will not be updated after they are created.
|
||||
// 2. The key is acquired by handle which protect the data accessibility
|
||||
// based on ref count. Once the method acquire handle successfully, the
|
||||
// accessor can safely access the data until the handle is released.
|
||||
//
|
||||
// IMPORTANT: The caller must keep the memory referenced by `slices` alive
|
||||
// from the time Get() returns until TaskHandle::Wait() completes. The
|
||||
// returned TaskHandle may capture raw pointers from the slices for
|
||||
// asynchronous data copy.
|
||||
tl::expected<ReadTaskHandle, ErrorCode> Get(
|
||||
const std::string& key, const std::vector<Slice>& slices);
|
||||
|
||||
tl::expected<ReadTaskHandle, ErrorCode> Get(
|
||||
const std::string& key,
|
||||
std::shared_ptr<ClientBufferAllocator> allocator);
|
||||
|
||||
/**
|
||||
* @brief Query the size of an object.
|
||||
* @param key Object key
|
||||
* @return Object size in bytes, or ErrorCode on failure
|
||||
*/
|
||||
tl::expected<size_t, ErrorCode> QueryObjectSize(const std::string& key);
|
||||
|
||||
tl::expected<void, ErrorCode> Delete(
|
||||
const std::string& key, std::optional<UUID> tier_id = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Get tier views from underlying tiered storage
|
||||
*/
|
||||
std::vector<TierView> GetTierViews() const;
|
||||
|
||||
// ================================================================
|
||||
// Remote data transfer — called by RPC service layer
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @brief Iterate all keys in batches.
|
||||
* Delegates to TieredBackend::ForEachKeyBatch().
|
||||
*/
|
||||
void ForEachKeyBatch(
|
||||
const std::function<bool(std::vector<ReplicaLocation>&&)>& callback)
|
||||
const;
|
||||
|
||||
/**
|
||||
* @brief Get hot key statistics
|
||||
*/
|
||||
AccessStats GetHotKeyStats() const;
|
||||
|
||||
/**
|
||||
* @brief Get all tier IDs where a key has replicas.
|
||||
*/
|
||||
std::vector<UUID> GetReplicaTierIds(const std::string& key) const;
|
||||
|
||||
/**
|
||||
* @brief Read data and transfer to remote destination buffers
|
||||
*
|
||||
* This is the core method for remote data access:
|
||||
* 1. Get data handle from TieredBackend
|
||||
* 2. Use TransferEngine to transfer data via RDMA to destination buffers
|
||||
*
|
||||
* @param key Object key to read
|
||||
* @param dest_buffers Destination buffers on remote client (Client A)
|
||||
* @return ErrorCode indicating success or failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> ReadRemoteData(
|
||||
const std::string& key,
|
||||
const std::vector<RemoteBufferDesc>& dest_buffers);
|
||||
|
||||
/**
|
||||
* @brief Write data from remote source buffers
|
||||
* @param key Object key to write
|
||||
* @param src_buffers Source buffers on remote client (Client A)
|
||||
* @param tier_id Optional tier ID (nullopt = use default tier selection)
|
||||
* @return UUID of the tier (segment) where data was written, or ErrorCode
|
||||
*/
|
||||
tl::expected<UUID, ErrorCode> WriteRemoteData(
|
||||
const std::string& key,
|
||||
const std::vector<RemoteBufferDesc>& src_buffers,
|
||||
std::optional<UUID> tier_id = std::nullopt);
|
||||
|
||||
// ================================================================
|
||||
// Utilities
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @brief Rectify stale read route by checking local key existence
|
||||
* and removing replica from master if key is not found locally.
|
||||
*
|
||||
* @param key Object key to rectify
|
||||
* @param tier_id Optional tier ID. If specified, only checks the given
|
||||
* tier; if nullopt, checks all tiers.
|
||||
*/
|
||||
void RectifyReadRoute(const std::string& key,
|
||||
std::optional<UUID> tier_id = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Set the callback for rectifying read routes in Master.
|
||||
* @param fn Callback invoked when key not found locally.
|
||||
*/
|
||||
void SetRectifyCallback(
|
||||
std::function<void(const std::string&, std::optional<UUID>)> fn);
|
||||
|
||||
bool Exist(const std::string& key,
|
||||
std::optional<UUID> tier_id = std::nullopt) const;
|
||||
|
||||
private:
|
||||
std::shared_mutex& GetKeyLock(const std::string& key) {
|
||||
size_t hash = std::hash<std::string>{}(key);
|
||||
return lock_shards_[hash % lock_shard_count_];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transfer data from local source to remote destination buffers
|
||||
* @param handle Local allocation handle (source)
|
||||
* @param dest_buffers Remote destination buffers
|
||||
* @return ErrorCode indicating success or failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> TransferDataToRemote(
|
||||
AllocationHandle handle,
|
||||
const std::vector<RemoteBufferDesc>& dest_buffers);
|
||||
|
||||
/**
|
||||
* @brief Transfer data from remote source buffers to local allocated space
|
||||
* @param handle Local allocation handle (destination)
|
||||
* @param src_buffers Remote source buffers
|
||||
* @return ErrorCode indicating success or failure. Any segment failure
|
||||
* will result in error (no partial success).
|
||||
*/
|
||||
tl::expected<void, ErrorCode> TransferDataFromRemote(
|
||||
AllocationHandle handle,
|
||||
const std::vector<RemoteBufferDesc>& src_buffers);
|
||||
|
||||
tl::expected<ReadTaskHandle, ErrorCode> BuildDataCopier(
|
||||
const AllocationHandle& handle, const std::string& key,
|
||||
const std::vector<Slice>& slices);
|
||||
|
||||
tl::expected<ReadTaskHandle, ErrorCode> BuildDataCopierViaTe(
|
||||
const AllocationHandle& handle, const std::vector<Slice>& slices);
|
||||
|
||||
tl::expected<ReadTaskHandle, ErrorCode> BuildDataCopierViaMemcpy(
|
||||
const AllocationHandle& handle, const std::string& key,
|
||||
const std::vector<Slice>& slices);
|
||||
|
||||
tl::expected<LocalCopyPlan, ErrorCode> BuildLocalCopyPlan(
|
||||
const std::string& key, const AllocationHandle& handle,
|
||||
const std::vector<Slice>& slices) const;
|
||||
// --- Put dispatch by transfer mode ---
|
||||
|
||||
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode> PutViaTe(
|
||||
const std::string& key, std::vector<Slice>& slices);
|
||||
|
||||
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode> PutViaMemcpy(
|
||||
const std::string& key, std::vector<Slice>& slices);
|
||||
|
||||
// --- Conversion helpers ---
|
||||
|
||||
std::vector<RemoteBufferDesc> SlicesToRemoteBufferDescs(
|
||||
const std::vector<Slice>& slices) const;
|
||||
|
||||
// --- TE transfer helpers ---
|
||||
struct TeSubmitResult {
|
||||
std::vector<std::tuple<Transport::BatchID, size_t, std::string>>
|
||||
transfer_batches;
|
||||
// Temp DRAM buffer used when source or destination is non-DRAM.
|
||||
// Non-null means a post-copy (for Write) or pre-copy (for Read) is
|
||||
// required.
|
||||
std::shared_ptr<void> temp_buffer;
|
||||
AllocationHandle handle; // Ensure local memory is not released
|
||||
};
|
||||
|
||||
tl::expected<TeSubmitResult, ErrorCode> SubmitTeTransferInternal(
|
||||
const AllocationHandle& handle,
|
||||
const std::vector<RemoteBufferDesc>& remote_buffers,
|
||||
Transport::TransferRequest::OpCode opcode);
|
||||
|
||||
/**
|
||||
* @brief Helper to wait for a transfer batch to complete
|
||||
* @param batch_id Batch ID to poll
|
||||
* @param num_tasks Number of tasks in the batch
|
||||
* @param segment_name Name of the segment for logging
|
||||
* @return ErrorCode indicating success or failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> WaitTransferBatch(
|
||||
Transport::BatchID batch_id, size_t num_tasks,
|
||||
const std::string& segment_endpoint);
|
||||
|
||||
/**
|
||||
* @brief Validate remote buffer descriptors
|
||||
* @param buffers Buffer descriptors to validate
|
||||
* @return ErrorCode if validation fails, otherwise OK
|
||||
*/
|
||||
tl::expected<void, ErrorCode> ValidateRemoteBuffers(
|
||||
const std::vector<RemoteBufferDesc>& buffers);
|
||||
|
||||
/**
|
||||
* @brief Prepare DRAM buffer for transfer from non-DRAM source
|
||||
* @param source_ptr Source data pointer
|
||||
* @param source_type Source memory type
|
||||
* @param total_size Total data size
|
||||
* @param backend TieredBackend for DataCopier access
|
||||
* @return Pair of (transfer_source_ptr, temp_buffer_owner) or error
|
||||
*/
|
||||
tl::expected<std::pair<void*, std::unique_ptr<void, void (*)(void*)>>,
|
||||
ErrorCode>
|
||||
PrepareDRAMTransferBuffer(void* source_ptr, MemoryType source_type,
|
||||
size_t total_size, TieredBackend* backend);
|
||||
|
||||
/**
|
||||
* @brief Prepare DRAM buffer for receiving data to non-DRAM destination
|
||||
* @param dest_ptr Destination data pointer
|
||||
* @param dest_type Destination memory type
|
||||
* @param total_size Total data size
|
||||
* @return Pair of (transfer_dest_ptr, temp_buffer_owner) or error
|
||||
*/
|
||||
tl::expected<std::pair<void*, std::unique_ptr<void, void (*)(void*)>>,
|
||||
ErrorCode>
|
||||
PrepareDRAMReceiveBuffer(void* dest_ptr, MemoryType dest_type,
|
||||
size_t total_size);
|
||||
|
||||
/**
|
||||
* @brief Copy data from DRAM buffer to non-DRAM tier
|
||||
* @param temp_buffer Temp DRAM buffer pointer
|
||||
* @param dest_ptr Destination pointer
|
||||
* @param dest_type Destination memory type
|
||||
* @param total_size Total data size
|
||||
* @param backend TieredBackend for DataCopier access
|
||||
* @return ErrorCode indicating success or failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> CopyFromDRAMBuffer(void* temp_buffer,
|
||||
void* dest_ptr,
|
||||
MemoryType dest_type,
|
||||
size_t total_size,
|
||||
TieredBackend* backend);
|
||||
|
||||
/**
|
||||
* @brief Submit transfer requests for a segment (without waiting)
|
||||
* @param segment_name Segment name (for logging)
|
||||
* @param seg Segment handle (already opened)
|
||||
* @param requests Transfer requests to submit
|
||||
* @return BatchID if successful, or error
|
||||
*/
|
||||
tl::expected<Transport::BatchID, ErrorCode> SubmitTransferRequests(
|
||||
const std::string& segment_endpoint, Transport::SegmentHandle seg,
|
||||
const std::vector<Transport::TransferRequest>& requests);
|
||||
|
||||
/**
|
||||
* @brief Wait for multiple transfer batches to complete
|
||||
* @param batches Vector of (batch_id, num_tasks, segment_endpoint) tuples
|
||||
* @return ErrorCode indicating success or failure. If any batch fails,
|
||||
* remaining batch IDs are freed and error is returned immediately.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> WaitAllTransferBatches(
|
||||
const std::vector<std::tuple<Transport::BatchID, size_t, std::string>>&
|
||||
batches);
|
||||
|
||||
// Wait for all tasks to reach a terminal state, then free the batch.
|
||||
void CancelBatchTETask(Transport::BatchID batch_id, size_t num_tasks);
|
||||
|
||||
private:
|
||||
std::unique_ptr<TieredBackend> tiered_backend_; // Owned by DataManager
|
||||
std::shared_ptr<TransferEngine> transfer_engine_; // Shared with Client
|
||||
|
||||
// Sharded locks for concurrent access
|
||||
// Configurable via MOONCAKE_DM_LOCK_SHARD_COUNT environment variable
|
||||
// (default: 1024)
|
||||
size_t lock_shard_count_;
|
||||
std::vector<std::shared_mutex> lock_shards_;
|
||||
|
||||
// Callback for rectifying stale read routes
|
||||
std::function<void(const std::string&, std::optional<UUID>)>
|
||||
rectify_wrong_route_fn_;
|
||||
|
||||
LocalTransferConfig local_transfer_config_;
|
||||
std::unique_ptr<AsyncMemcpyExecutor> async_memcpy_executor_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_config_builder.h"
|
||||
|
||||
#include <ylt/coro_rpc/coro_rpc_client.hpp>
|
||||
|
||||
#include "pyclient.h"
|
||||
#include "real_client.h"
|
||||
#include <memory>
|
||||
|
||||
namespace mooncake {
|
||||
|
|
@ -12,30 +13,30 @@ class ShmHelper {
|
|||
public:
|
||||
struct ShmSegment {
|
||||
int fd = -1;
|
||||
void *base_addr = nullptr;
|
||||
void* base_addr = nullptr;
|
||||
size_t size = 0;
|
||||
std::string name;
|
||||
bool registered = false;
|
||||
bool is_local = false;
|
||||
};
|
||||
|
||||
static ShmHelper *getInstance();
|
||||
static ShmHelper* getInstance();
|
||||
|
||||
void *allocate(size_t size);
|
||||
int free(void *addr);
|
||||
void* allocate(size_t size);
|
||||
int free(void* addr);
|
||||
|
||||
bool cleanup();
|
||||
|
||||
// Get the shm that contains the given address
|
||||
// Returns a shared_ptr to ensure the segment remains valid
|
||||
std::shared_ptr<ShmSegment> get_shm(void *addr);
|
||||
std::shared_ptr<ShmSegment> get_shm(void* addr);
|
||||
|
||||
const std::vector<std::shared_ptr<ShmSegment>> &get_shms() const {
|
||||
const std::vector<std::shared_ptr<ShmSegment>>& get_shms() const {
|
||||
return shms_;
|
||||
}
|
||||
|
||||
ShmHelper(const ShmHelper &) = delete;
|
||||
ShmHelper &operator=(const ShmHelper &) = delete;
|
||||
ShmHelper(const ShmHelper&) = delete;
|
||||
ShmHelper& operator=(const ShmHelper&) = delete;
|
||||
|
||||
private:
|
||||
ShmHelper();
|
||||
|
|
@ -52,106 +53,104 @@ class DummyClient : public PyClient {
|
|||
|
||||
int64_t unregister_shm();
|
||||
|
||||
int setup_real(const std::string &local_hostname,
|
||||
const std::string &metadata_server,
|
||||
size_t global_segment_size, size_t local_buffer_size,
|
||||
const std::string &protocol, const std::string &rdma_devices,
|
||||
const std::string &master_server_addr,
|
||||
const std::shared_ptr<TransferEngine> &transfer_engine,
|
||||
const std::string &ipc_socket_path) {
|
||||
// Dummy client does not support real setup
|
||||
return -1;
|
||||
};
|
||||
int setup(DummyClientConfig& config);
|
||||
|
||||
int setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
|
||||
const std::string &server_address,
|
||||
const std::string &ipc_socket_path);
|
||||
|
||||
int initAll(const std::string &protocol, const std::string &device_name,
|
||||
size_t mount_segment_size) {
|
||||
int initAll(const std::string& protocol, const std::string& device_name,
|
||||
size_t mount_segment_size) override {
|
||||
// Dummy client does not support real setup
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t alloc_from_mem_pool(size_t size);
|
||||
uint64_t alloc_from_mem_pool(size_t size) override;
|
||||
|
||||
int put(const std::string &key, std::span<const char> value,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
// if a dummy client has connected to a real client,
|
||||
// return the mode of real client
|
||||
DeploymentMode deployment_mode() const override { return deployment_mode_; }
|
||||
|
||||
int register_buffer(void *buffer, size_t size);
|
||||
int put(const std::string& key, std::span<const char> value,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
int unregister_buffer(void *buffer);
|
||||
int register_buffer(void* buffer, size_t size) override;
|
||||
|
||||
int64_t get_into(const std::string &key, void *buffer, size_t size);
|
||||
int unregister_buffer(void* buffer) override;
|
||||
|
||||
std::vector<int64_t> batch_get_into(const std::vector<std::string> &keys,
|
||||
const std::vector<void *> &buffers,
|
||||
const std::vector<size_t> &sizes);
|
||||
int64_t get_into(const std::string& key, void* buffer, size_t size,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::vector<int64_t> batch_get_into(
|
||||
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::vector<int> batch_get_into_multi_buffers(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::vector<void *>> &all_buffers,
|
||||
const std::vector<std::vector<size_t>> &all_sizes,
|
||||
bool prefer_same_node);
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
bool aggregate_same_segment_task,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
int put_from(const std::string &key, void *buffer, size_t size,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
int put_from(const std::string& key, void* buffer, size_t size,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
int put_from_with_metadata(
|
||||
const std::string &key, void *buffer, void *metadata_buffer,
|
||||
size_t size, size_t metadata_size,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
int put_from_with_metadata(const std::string& key, void* buffer,
|
||||
void* metadata_buffer, size_t size,
|
||||
size_t metadata_size,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
std::vector<int> batch_put_from(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<void *> &buffers, const std::vector<size_t> &sizes,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
std::vector<int> batch_put_from(const std::vector<std::string>& keys,
|
||||
const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
std::vector<int> batch_put_from_multi_buffers(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::vector<void *>> &all_buffers,
|
||||
const std::vector<std::vector<size_t>> &all_sizes,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
std::shared_ptr<BufferHandle> get_buffer(const std::string &key);
|
||||
std::shared_ptr<BufferHandle> get_buffer(
|
||||
const std::string& key, const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::tuple<uint64_t, size_t> get_buffer_info(const std::string &key);
|
||||
std::tuple<uint64_t, size_t> get_buffer_info(
|
||||
const std::string& key, const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::vector<std::shared_ptr<BufferHandle>> batch_get_buffer(
|
||||
const std::vector<std::string> &keys);
|
||||
const std::vector<std::string>& keys,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
int put_parts(const std::string &key,
|
||||
int put_parts(const std::string& key,
|
||||
std::vector<std::span<const char>> values,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
const WriteConfig& config) override;
|
||||
|
||||
int put_batch(const std::vector<std::string> &keys,
|
||||
const std::vector<std::span<const char>> &values,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
int put_batch(const std::vector<std::string>& keys,
|
||||
const std::vector<std::span<const char>>& values,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
[[nodiscard]] std::string get_hostname() const;
|
||||
[[nodiscard]] std::string get_hostname() const override;
|
||||
|
||||
int remove(const std::string &key);
|
||||
int remove(const std::string& key) override;
|
||||
|
||||
long removeByRegex(const std::string &str);
|
||||
long removeByRegex(const std::string& str) override;
|
||||
|
||||
long removeAll();
|
||||
long removeAll() override;
|
||||
|
||||
int isExist(const std::string &key);
|
||||
int isExist(const std::string& key) override;
|
||||
|
||||
std::vector<int> batchIsExist(const std::vector<std::string> &keys);
|
||||
std::vector<int> batchIsExist(
|
||||
const std::vector<std::string>& keys) override;
|
||||
|
||||
int64_t getSize(const std::string &key);
|
||||
int64_t getSize(const std::string& key) override;
|
||||
|
||||
std::map<std::string, std::vector<Replica::Descriptor>>
|
||||
batch_get_replica_desc(const std::vector<std::string> &keys);
|
||||
std::vector<Replica::Descriptor> get_replica_desc(const std::string &key);
|
||||
batch_get_replica_desc(const std::vector<std::string>& keys);
|
||||
std::vector<Replica::Descriptor> get_replica_desc(const std::string& key);
|
||||
|
||||
int tearDownAll();
|
||||
int tearDownAll() override;
|
||||
|
||||
private:
|
||||
ErrorCode connect(const std::string &server_address);
|
||||
ErrorCode connect(const std::string& server_address);
|
||||
|
||||
int register_shm_via_ipc(const ShmHelper::ShmSegment *shm,
|
||||
int register_shm_via_ipc(const ShmHelper::ShmSegment* shm,
|
||||
bool is_local = false);
|
||||
|
||||
/**
|
||||
|
|
@ -164,7 +163,7 @@ class DummyClient : public PyClient {
|
|||
*/
|
||||
template <auto ServiceMethod, typename ReturnType, typename... Args>
|
||||
[[nodiscard]] tl::expected<ReturnType, ErrorCode> invoke_rpc(
|
||||
Args &&...args);
|
||||
Args&&... args);
|
||||
|
||||
/**
|
||||
* @brief Generic RPC invocation helper for batch operations
|
||||
|
|
@ -177,7 +176,7 @@ class DummyClient : public PyClient {
|
|||
*/
|
||||
template <auto ServiceMethod, typename ResultType, typename... Args>
|
||||
[[nodiscard]] std::vector<tl::expected<ResultType, ErrorCode>>
|
||||
invoke_batch_rpc(size_t input_size, Args &&...args);
|
||||
invoke_batch_rpc(size_t input_size, Args&&... args);
|
||||
|
||||
/**
|
||||
* @brief Accessor for the coro_rpc_client pool. Since coro_rpc_client
|
||||
|
|
@ -217,8 +216,10 @@ class DummyClient : public PyClient {
|
|||
// The address which is passed to the coro_rpc_client
|
||||
std::string client_addr_param_ GUARDED_BY(connect_mutex_);
|
||||
|
||||
DeploymentMode deployment_mode_ = DeploymentMode::UNKNOWN;
|
||||
|
||||
// For shared memory management
|
||||
ShmHelper *shm_helper_ = nullptr;
|
||||
ShmHelper* shm_helper_ = nullptr;
|
||||
std::string ipc_socket_path_;
|
||||
|
||||
// For high availability
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_service.h"
|
||||
#include "centralized_client_service.h"
|
||||
#include "client_buffer.hpp"
|
||||
#include "storage_backend.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class CentralizedClientService;
|
||||
|
||||
class FileStorage {
|
||||
public:
|
||||
FileStorage(std::shared_ptr<Client> client,
|
||||
FileStorage(std::shared_ptr<CentralizedClientService> client,
|
||||
const std::string& local_rpc_addr,
|
||||
const FileStorageConfig& config);
|
||||
~FileStorage();
|
||||
|
|
@ -80,7 +82,7 @@ class FileStorage {
|
|||
const std::vector<std::string>& keys,
|
||||
const std::vector<int64_t>& sizes);
|
||||
|
||||
std::shared_ptr<Client> client_;
|
||||
std::shared_ptr<CentralizedClientService> client_;
|
||||
std::string local_rpc_addr_;
|
||||
FileStorageConfig config_;
|
||||
std::shared_ptr<StorageBackendInterface> storage_backend_;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
|
||||
#include "async_metadata_notifier.h"
|
||||
#include "data_manager.h"
|
||||
#include "p2p_master_client.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @class HARecoveryManager
|
||||
* @brief Manages client-side HA state machine and multi-phase recovery pipeline
|
||||
* for Master crash recovery.
|
||||
*
|
||||
* State machine (2 events: MASTER_UNREACHABLE, MASTER_REACHABLE):
|
||||
* FULL ──MASTER_UNREACHABLE─────> DEGRADED
|
||||
* FULL ──MASTER_REACHABLE───────> SYNCING (Master restarted)
|
||||
* DEGRADED ──MASTER_REACHABLE───> SYNCING (always full re-sync)
|
||||
* SYNCING ──recovery complete───> FULL
|
||||
* SYNCING ──MASTER_UNREACHABLE──> DEGRADED
|
||||
* SYNCING ──MASTER_REACHABLE────> SYNCING (restart pipeline)
|
||||
*
|
||||
* Thread safety:
|
||||
* state_ is atomic for lock-free reads on data-path hot path.
|
||||
* mutex_ protects transitions and recovery thread lifecycle.
|
||||
* need_abort_ (shared atomic bool) signals the recovery thread to stop
|
||||
* without requiring mutex_, enabling safe join from HandleEvent.
|
||||
*/
|
||||
class HARecoveryManager {
|
||||
public:
|
||||
HARecoveryManager(const UUID& client_id, P2PMasterClient& master_client,
|
||||
std::optional<DataManager>& data_manager,
|
||||
std::unique_ptr<AsyncMetadataNotifier>& notifier,
|
||||
std::atomic<ViewVersionId>& view_version,
|
||||
HAClientState initial_state = HAClientState::FULL);
|
||||
~HARecoveryManager();
|
||||
|
||||
HARecoveryManager(const HARecoveryManager&) = delete;
|
||||
HARecoveryManager& operator=(const HARecoveryManager&) = delete;
|
||||
|
||||
void Stop();
|
||||
|
||||
bool IsDegraded() const {
|
||||
return state_.load(std::memory_order_acquire) ==
|
||||
HAClientState::DEGRADED;
|
||||
}
|
||||
|
||||
HAClientState GetState() const {
|
||||
return state_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Mark that P2PClientService::Init has completed. Called at the end
|
||||
* of Init(). Recovery thread will wait for this before accessing
|
||||
* data_manager_.
|
||||
*/
|
||||
void SetReadyForRecovery() {
|
||||
ready_for_recovery_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void HandleEvent(HAEvent event);
|
||||
|
||||
tl::expected<void, ErrorCode> SetSyncCompleted();
|
||||
|
||||
private:
|
||||
using AbortToken = std::shared_ptr<std::atomic<bool>>;
|
||||
|
||||
void TransitionState(HAClientState to, const std::string& reason);
|
||||
void StartRecoveryThread();
|
||||
void RecoveryPipelineMain(AbortToken need_abort);
|
||||
|
||||
/**
|
||||
* @brief Retry enqueue until success or abort is signalled.
|
||||
* Hot keys use the normal (high-priority) queue; recovery keys use
|
||||
* the recovery queue. Sleeps 10 ms between attempts so normal
|
||||
* writes keep priority.
|
||||
* @return true on success, false if aborted.
|
||||
*/
|
||||
bool EnqueueWithRetry(const std::string& key, const UUID& tier_id,
|
||||
size_t size, bool is_hot,
|
||||
const AbortToken& need_abort);
|
||||
|
||||
/**
|
||||
* @brief Wait for P2PClientService::Init to complete.
|
||||
* Checks ready_for_recovery_ and data_manager_ availability.
|
||||
* @return true if ready, false if aborted or data_manager not initialized.
|
||||
*/
|
||||
bool WaitForReady(const AbortToken& need_abort);
|
||||
|
||||
const UUID& client_id_;
|
||||
P2PMasterClient& master_client_;
|
||||
std::optional<DataManager>& data_manager_;
|
||||
std::unique_ptr<AsyncMetadataNotifier>& notifier_;
|
||||
std::atomic<ViewVersionId>& view_version_;
|
||||
|
||||
std::atomic<HAClientState> state_;
|
||||
std::atomic<bool> ready_for_recovery_{
|
||||
false}; // Set true when P2PClientService::Init completes
|
||||
std::mutex mutex_; // protects transitions + recovery thread lifecycle
|
||||
std::thread recovery_thread_;
|
||||
AbortToken need_abort_; // signals recovery thread to exit
|
||||
|
||||
// Separate mutex/CV for interruptible sleeps inside the recovery thread.
|
||||
// Must NOT be mutex_ (HandleEvent holds mutex_ while joining the thread).
|
||||
std::mutex abort_mutex_;
|
||||
std::condition_variable abort_cv_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
#pragma once
|
||||
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
// =====================================================================
|
||||
// Heartbeat Task Types
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* @brief Types of tasks that can be carried in a heartbeat request.
|
||||
* Only lightweight info-sync tasks; heavy operations like RegisterClient
|
||||
* should use specifical RPC.
|
||||
*/
|
||||
enum class HeartbeatTaskType {
|
||||
SYNC_SEGMENT_META, // Sync segment usage metadata (for P2P structure)
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// Heartbeat Task Params
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* @brief Usage info for a single tier (e.g. segment).
|
||||
*/
|
||||
struct TierUsageInfo {
|
||||
UUID segment_id;
|
||||
size_t usage = 0;
|
||||
};
|
||||
YLT_REFL(TierUsageInfo, segment_id, usage);
|
||||
|
||||
/**
|
||||
* @brief Param for SYNC_SEGMENT_META task.
|
||||
*/
|
||||
struct SyncSegmentMetaParam {
|
||||
std::vector<TierUsageInfo> tier_usages;
|
||||
};
|
||||
YLT_REFL(SyncSegmentMetaParam, tier_usages);
|
||||
|
||||
// =====================================================================
|
||||
// HeartbeatTask
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* @brief A single task carried in a heartbeat request.
|
||||
*/
|
||||
struct HeartbeatTask {
|
||||
using ParamVariant = std::variant<SyncSegmentMetaParam>;
|
||||
|
||||
HeartbeatTask() = default;
|
||||
|
||||
HeartbeatTask(HeartbeatTaskType type, ParamVariant param)
|
||||
: type_(type), param_(std::move(param)) {}
|
||||
|
||||
HeartbeatTaskType type_;
|
||||
ParamVariant param_;
|
||||
};
|
||||
YLT_REFL(HeartbeatTask, type_, param_);
|
||||
|
||||
/**
|
||||
* @brief Detailed result for SYNC_SEGMENT_META task.
|
||||
*/
|
||||
struct SyncSegmentMetaResult {
|
||||
struct SubResult {
|
||||
UUID segment_id;
|
||||
ErrorCode error = ErrorCode::OK;
|
||||
};
|
||||
std::vector<SubResult> sub_results;
|
||||
};
|
||||
YLT_REFL(SyncSegmentMetaResult::SubResult, segment_id, error);
|
||||
YLT_REFL(SyncSegmentMetaResult, sub_results);
|
||||
|
||||
struct HeartbeatTaskResult {
|
||||
using DetailVariant = std::variant<std::monostate, SyncSegmentMetaResult>;
|
||||
|
||||
HeartbeatTaskType type;
|
||||
ErrorCode error = ErrorCode::OK;
|
||||
DetailVariant detail;
|
||||
};
|
||||
YLT_REFL(HeartbeatTaskResult, type, error, detail);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include <coroutine>
|
||||
#include <async_simple/coro/FutureAwaiter.h>
|
||||
#include <async_simple/coro/Lazy.h>
|
||||
#include <async_simple/coro/SyncAwait.h>
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
@ -9,13 +15,14 @@
|
|||
#include <ylt/coro_io/client_pool.hpp>
|
||||
|
||||
#include "client_metric.h"
|
||||
#include "replica.h"
|
||||
#include "types.h"
|
||||
#include "rpc_types.h"
|
||||
#include "master_metric_manager.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
template <auto Method>
|
||||
struct RpcNameTraits;
|
||||
|
||||
static const std::string kDefaultMasterAddress = "localhost:50051";
|
||||
|
||||
/**
|
||||
|
|
@ -23,20 +30,7 @@ static const std::string kDefaultMasterAddress = "localhost:50051";
|
|||
*/
|
||||
class MasterClient {
|
||||
public:
|
||||
MasterClient(const UUID& client_id, MasterClientMetric* metrics = nullptr)
|
||||
: client_id_(client_id), metrics_(metrics) {
|
||||
coro_io::client_pool<coro_rpc::coro_rpc_client>::pool_config
|
||||
pool_conf{};
|
||||
const char* value = std::getenv("MC_RPC_PROTOCOL");
|
||||
if (value && std::string_view(value) == "rdma") {
|
||||
pool_conf.client_config.socket_config =
|
||||
coro_io::ib_socket_t::config_t{};
|
||||
}
|
||||
client_pools_ =
|
||||
std::make_shared<coro_io::client_pools<coro_rpc::coro_rpc_client>>(
|
||||
pool_conf);
|
||||
}
|
||||
~MasterClient();
|
||||
virtual ~MasterClient() = default;
|
||||
|
||||
MasterClient(const MasterClient&) = delete;
|
||||
MasterClient& operator=(const MasterClient&) = delete;
|
||||
|
|
@ -64,6 +58,30 @@ class MasterClient {
|
|||
*/
|
||||
[[nodiscard]] std::vector<tl::expected<bool, ErrorCode>> BatchExistKey(
|
||||
const std::vector<std::string>& object_keys);
|
||||
/**
|
||||
* @brief Gets replica list for an object
|
||||
* @param object_key Key to query
|
||||
* @param config Filter configuration for getting replica list
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<GetReplicaListResponse, ErrorCode>
|
||||
GetReplicaList(const std::string& key,
|
||||
const GetReplicaListRequestConfig& config =
|
||||
GetReplicaListRequestConfig());
|
||||
|
||||
[[nodiscard]] async_simple::coro::Lazy<
|
||||
tl::expected<GetReplicaListResponse, ErrorCode>>
|
||||
AsyncGetReplicaList(const std::string& key,
|
||||
const GetReplicaListRequestConfig& config =
|
||||
GetReplicaListRequestConfig());
|
||||
|
||||
/**
|
||||
* @brief Batch query read routes
|
||||
*/
|
||||
[[nodiscard]] std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
|
||||
BatchGetReplicaList(const std::vector<std::string>& keys,
|
||||
const GetReplicaListRequestConfig& config =
|
||||
GetReplicaListRequestConfig());
|
||||
|
||||
/**
|
||||
* @brief Calculate cache hit rate metrics
|
||||
|
|
@ -84,29 +102,6 @@ class MasterClient {
|
|||
ErrorCode>
|
||||
BatchQueryIp(const std::vector<UUID>& client_ids);
|
||||
|
||||
/**
|
||||
* @brief Batch clear KV cache for specified object keys on a specific
|
||||
* segment for a given client.
|
||||
* @param object_keys Vector of object key strings to clear.
|
||||
* @param client_id The UUID of the client that owns the object keys.
|
||||
* @param segment_name The name of the segment (storage device) to clear
|
||||
* from.
|
||||
* @return An expected object containing a vector of successfully cleared
|
||||
* object keys on success, or an ErrorCode on failure.
|
||||
*/
|
||||
[[nodiscard]] tl::expected<std::vector<std::string>, ErrorCode>
|
||||
BatchReplicaClear(const std::vector<std::string>& object_keys,
|
||||
const UUID& client_id, const std::string& segment_name);
|
||||
|
||||
/**
|
||||
* @brief Gets object metadata without transferring data
|
||||
* @param object_key Key to query
|
||||
* @param object_info Output parameter for object metadata
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<GetReplicaListResponse, ErrorCode>
|
||||
GetReplicaList(const std::string& object_key);
|
||||
|
||||
/**
|
||||
* @brief Retrieves replica lists for object keys that match a regex
|
||||
* pattern.
|
||||
|
|
@ -119,77 +114,6 @@ class MasterClient {
|
|||
ErrorCode>
|
||||
GetReplicaListByRegex(const std::string& str);
|
||||
|
||||
/**
|
||||
* @brief Gets object metadata without transferring data
|
||||
* @param object_keys Keys to query
|
||||
* @param object_infos Output parameter for object metadata
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]]
|
||||
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
|
||||
BatchGetReplicaList(const std::vector<std::string>& object_keys);
|
||||
|
||||
/**
|
||||
* @brief Starts a put operation
|
||||
* @param key Object key
|
||||
* @param slice_lengths Vector of slice lengths
|
||||
* @param value_length Total value length
|
||||
* @param config Replication configuration
|
||||
* @return tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
|
||||
* indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
|
||||
PutStart(const std::string& key, const std::vector<size_t>& slice_lengths,
|
||||
const ReplicateConfig& config);
|
||||
|
||||
/**
|
||||
* @brief Starts a batch of put operations for N objects
|
||||
* @param keys Vector of object key
|
||||
* @param value_lengths Vector of total value lengths
|
||||
* @param slice_lengths Vector of vectors of slice lengths
|
||||
* @param config Replication configuration
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] std::vector<
|
||||
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
||||
BatchPutStart(const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<uint64_t>>& slice_lengths,
|
||||
const ReplicateConfig& config);
|
||||
|
||||
/**
|
||||
* @brief Ends a put operation
|
||||
* @param key Object key
|
||||
* @param replica_type Type of replica (memory or disk)
|
||||
* @return tl::expected<void, ErrorCode> indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> PutEnd(
|
||||
const std::string& key, ReplicaType replica_type);
|
||||
|
||||
/**
|
||||
* @brief Ends a put operation for a batch of objects
|
||||
* @param keys Vector of object keys
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
|
||||
const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief Revokes a put operation
|
||||
* @param key Object key
|
||||
* @param replica_type Type of replica (memory or disk)
|
||||
* @return tl::expected<void, ErrorCode> indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> PutRevoke(
|
||||
const std::string& key, ReplicaType replica_type);
|
||||
|
||||
/**
|
||||
* @brief Revokes a put operation for a batch of objects
|
||||
* @param keys Vector of object keys
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
|
||||
const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief Removes an object and all its replicas
|
||||
* @param key Key to remove
|
||||
|
|
@ -212,25 +136,6 @@ class MasterClient {
|
|||
*/
|
||||
[[nodiscard]] tl::expected<long, ErrorCode> RemoveAll();
|
||||
|
||||
/**
|
||||
* @brief Registers a segment to master for allocation
|
||||
* @param segment Segment to register
|
||||
* @return tl::expected<void, ErrorCode> indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> MountSegment(
|
||||
const Segment& segment);
|
||||
|
||||
/**
|
||||
* @brief Re-mount segments, invoked when the client is the first time to
|
||||
* connect to the master or the client Ping TTL is expired and need
|
||||
* to remount. This function is idempotent. Client should retry if the
|
||||
* return code is not ErrorCode::OK.
|
||||
* @param segments Segments to remount
|
||||
* @return tl::expected<void, ErrorCode> indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> ReMountSegment(
|
||||
const std::vector<Segment>& segments);
|
||||
|
||||
/**
|
||||
* @brief Unregisters a memory segment from master
|
||||
* @param segment_id ID of the segment to unmount
|
||||
|
|
@ -240,51 +145,51 @@ class MasterClient {
|
|||
const UUID& segment_id);
|
||||
|
||||
/**
|
||||
* @brief Gets the cluster ID for the current client to use as subdirectory
|
||||
* name
|
||||
* @return GetClusterIdResponse containing the cluster ID
|
||||
* @brief Queries the status of a client.
|
||||
* @param client_id The UUID of the client to query.
|
||||
* @return tl::expected<QueryClientStatusResponse, ErrorCode>
|
||||
*/
|
||||
[[nodiscard]] tl::expected<std::string, ErrorCode> GetFsdir();
|
||||
|
||||
[[nodiscard]] tl::expected<GetStorageConfigResponse, ErrorCode>
|
||||
GetStorageConfig();
|
||||
[[nodiscard]] tl::expected<QueryClientStatusResponse, ErrorCode>
|
||||
QueryClientStatus(const UUID& client_id);
|
||||
|
||||
/**
|
||||
* @brief Pings master to check its availability
|
||||
* @return tl::expected<PingResponse, ErrorCode>
|
||||
* @brief Sends heartbeat to master to maintain client liveness
|
||||
* @return tl::expected<HeartbeatResponse, ErrorCode>
|
||||
* containing view version and client status
|
||||
*/
|
||||
[[nodiscard]] tl::expected<PingResponse, ErrorCode> Ping();
|
||||
[[nodiscard]] tl::expected<HeartbeatResponse, ErrorCode> Heartbeat(
|
||||
const HeartbeatRequest& req);
|
||||
|
||||
/**
|
||||
* @brief Mounts a local disk segment into the master.
|
||||
* @param enable_offloading If true, enables offloading (write-to-file).
|
||||
* @brief Registers a segment to master for allocation
|
||||
* @param segment Segment to register
|
||||
* @return tl::expected<void, ErrorCode> indicating success/failure
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> MountLocalDiskSegment(
|
||||
const UUID& client_id, bool enable_offloading);
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> MountSegment(
|
||||
const Segment& segment);
|
||||
|
||||
/**
|
||||
* @brief Heartbeat call to collect object-level statistics and retrieve the
|
||||
* set of non-persisted objects.
|
||||
* @param enable_offloading Indicates whether persistence is enabled for
|
||||
* this segment.
|
||||
* @brief Register client with the master on startup.
|
||||
* @param req request with registration information
|
||||
* @return tl::expected<RegisterClientResponse, ErrorCode>
|
||||
*/
|
||||
[[nodiscard]] tl::expected<std::unordered_map<std::string, int64_t>,
|
||||
ErrorCode>
|
||||
OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading);
|
||||
[[nodiscard]] tl::expected<RegisterClientResponse, ErrorCode>
|
||||
RegisterClient(const RegisterClientRequest& req);
|
||||
|
||||
/**
|
||||
* @brief Adds multiple new objects to a specified client in batch.
|
||||
* @param keys A list of object keys (names) that were successfully
|
||||
* offloaded.
|
||||
* @param metadatas The corresponding metadata for each offloaded object,
|
||||
* including size, storage location, etc.
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas);
|
||||
|
||||
private:
|
||||
protected:
|
||||
MasterClient(const UUID& client_id, MasterClientMetric* metrics = nullptr)
|
||||
: client_id_(client_id), metrics_(metrics) {
|
||||
coro_io::client_pool<coro_rpc::coro_rpc_client>::pool_config
|
||||
pool_conf{};
|
||||
const char* value = std::getenv("MC_RPC_PROTOCOL");
|
||||
if (value && std::string_view(value) == "rdma") {
|
||||
pool_conf.client_config.socket_config =
|
||||
coro_io::ib_socket_t::config_t{};
|
||||
}
|
||||
client_pools_ =
|
||||
std::make_shared<coro_io::client_pools<coro_rpc::coro_rpc_client>>(
|
||||
pool_conf);
|
||||
}
|
||||
/**
|
||||
* @brief Generic RPC invocation helper for single-result operations
|
||||
* @tparam ServiceMethod Pointer to WrappedMasterService member function
|
||||
|
|
@ -293,9 +198,49 @@ class MasterClient {
|
|||
* @param args Arguments to pass to the RPC call
|
||||
* @return The result of the RPC call
|
||||
*/
|
||||
template <auto ServiceMethod, typename ReturnType, typename... Args>
|
||||
[[nodiscard]] async_simple::coro::Lazy<tl::expected<ReturnType, ErrorCode>>
|
||||
invoke_rpc_async(Args&&... args) {
|
||||
auto pool = client_accessor_.GetClientPool();
|
||||
|
||||
// Increment RPC counter
|
||||
if (metrics_) {
|
||||
metrics_->rpc_count.inc({RpcNameTraits<ServiceMethod>::value});
|
||||
}
|
||||
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
auto ret = co_await pool->send_request(
|
||||
[&](coro_io::client_reuse_hint, coro_rpc::coro_rpc_client& client) {
|
||||
return client.send_request<ServiceMethod>(
|
||||
std::forward<Args>(args)...);
|
||||
});
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "Client not available";
|
||||
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
|
||||
}
|
||||
auto result = co_await std::move(ret.value());
|
||||
if (!result) {
|
||||
LOG(ERROR) << "RPC call failed: " << result.error().msg;
|
||||
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
|
||||
}
|
||||
if (metrics_) {
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto latency =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
end_time - start_time);
|
||||
metrics_->rpc_latency.observe({RpcNameTraits<ServiceMethod>::value},
|
||||
latency.count());
|
||||
}
|
||||
co_return result->result();
|
||||
}
|
||||
|
||||
template <auto ServiceMethod, typename ReturnType, typename... Args>
|
||||
[[nodiscard]] tl::expected<ReturnType, ErrorCode> invoke_rpc(
|
||||
Args&&... args);
|
||||
Args&&... args) {
|
||||
return async_simple::coro::syncAwait(
|
||||
invoke_rpc_async<ServiceMethod, ReturnType>(
|
||||
std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Generic RPC invocation helper for batch operations
|
||||
|
|
@ -308,13 +253,60 @@ class MasterClient {
|
|||
*/
|
||||
template <auto ServiceMethod, typename ResultType, typename... Args>
|
||||
[[nodiscard]] std::vector<tl::expected<ResultType, ErrorCode>>
|
||||
invoke_batch_rpc(size_t input_size, Args&&... args);
|
||||
invoke_batch_rpc(size_t input_size, Args&&... args) {
|
||||
auto pool = client_accessor_.GetClientPool();
|
||||
|
||||
// Increment RPC counter
|
||||
if (metrics_) {
|
||||
metrics_->rpc_count.inc({RpcNameTraits<ServiceMethod>::value});
|
||||
}
|
||||
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
return async_simple::coro::syncAwait(
|
||||
[&]() -> async_simple::coro::Lazy<
|
||||
std::vector<tl::expected<ResultType, ErrorCode>>> {
|
||||
auto ret = co_await pool->send_request(
|
||||
[&](coro_io::client_reuse_hint,
|
||||
coro_rpc::coro_rpc_client& client) {
|
||||
return client.send_request<ServiceMethod>(
|
||||
std::forward<Args>(args)...);
|
||||
});
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "Client not available";
|
||||
co_return std::vector<tl::expected<ResultType, ErrorCode>>(
|
||||
input_size, tl::make_unexpected(ErrorCode::RPC_FAIL));
|
||||
}
|
||||
auto result = co_await std::move(ret.value());
|
||||
if (!result) {
|
||||
LOG(ERROR)
|
||||
<< "Batch RPC call failed: " << result.error().msg;
|
||||
std::vector<tl::expected<ResultType, ErrorCode>>
|
||||
error_results;
|
||||
error_results.reserve(input_size);
|
||||
for (size_t i = 0; i < input_size; ++i) {
|
||||
error_results.emplace_back(
|
||||
tl::make_unexpected(ErrorCode::RPC_FAIL));
|
||||
}
|
||||
co_return error_results;
|
||||
}
|
||||
if (metrics_) {
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto latency =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
end_time - start_time);
|
||||
metrics_->rpc_latency.observe(
|
||||
{RpcNameTraits<ServiceMethod>::value}, latency.count());
|
||||
}
|
||||
co_return result->result();
|
||||
}());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Accessor for the coro_rpc_client pool. Since coro_rpc_client pool
|
||||
* cannot reconnect to a different address, a new coro_rpc_client pool is
|
||||
* created if the address is different from the current one.
|
||||
*/
|
||||
protected:
|
||||
class RpcClientAccessor {
|
||||
public:
|
||||
void SetClientPool(
|
||||
|
|
@ -335,6 +327,8 @@ class MasterClient {
|
|||
std::shared_ptr<coro_io::client_pool<coro_rpc::coro_rpc_client>>
|
||||
client_pool_;
|
||||
};
|
||||
|
||||
protected:
|
||||
RpcClientAccessor client_accessor_;
|
||||
|
||||
// The client identification.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ struct MasterConfig {
|
|||
double eviction_ratio;
|
||||
double eviction_high_watermark_ratio;
|
||||
int64_t client_live_ttl_sec;
|
||||
int64_t client_crashed_ttl_sec = -1;
|
||||
|
||||
bool enable_ha;
|
||||
bool enable_offload;
|
||||
|
|
@ -45,6 +46,9 @@ struct MasterConfig {
|
|||
// Storage backend eviction configuration
|
||||
bool enable_disk_eviction;
|
||||
uint64_t quota_bytes;
|
||||
uint64_t max_replicas_per_key;
|
||||
|
||||
std::string deployment_mode;
|
||||
};
|
||||
|
||||
class MasterServiceSupervisorConfig {
|
||||
|
|
@ -60,6 +64,7 @@ class MasterServiceSupervisorConfig {
|
|||
RequiredParam<double> eviction_high_watermark_ratio{
|
||||
"eviction_high_watermark_ratio"};
|
||||
RequiredParam<int64_t> client_live_ttl_sec{"client_live_ttl_sec"};
|
||||
RequiredParam<int64_t> client_crashed_ttl_sec{"client_crashed_ttl_sec"};
|
||||
RequiredParam<bool> enable_offload{"enable_offload"};
|
||||
RequiredParam<int> rpc_port{"rpc_port"};
|
||||
RequiredParam<size_t> rpc_thread_num{"rpc_thread_num"};
|
||||
|
|
@ -79,6 +84,8 @@ class MasterServiceSupervisorConfig {
|
|||
uint64_t put_start_release_timeout_sec = DEFAULT_PUT_START_RELEASE_TIMEOUT;
|
||||
bool enable_disk_eviction = true;
|
||||
uint64_t quota_bytes = 0;
|
||||
uint64_t max_replicas_per_key = 1;
|
||||
DeploymentMode deployment_mode = DeploymentMode::CENTRALIZATION;
|
||||
|
||||
MasterServiceSupervisorConfig() = default;
|
||||
|
||||
|
|
@ -94,6 +101,7 @@ class MasterServiceSupervisorConfig {
|
|||
eviction_ratio = config.eviction_ratio;
|
||||
eviction_high_watermark_ratio = config.eviction_high_watermark_ratio;
|
||||
client_live_ttl_sec = config.client_live_ttl_sec;
|
||||
client_crashed_ttl_sec = config.client_crashed_ttl_sec;
|
||||
enable_offload = config.enable_offload;
|
||||
rpc_port = static_cast<int>(config.rpc_port);
|
||||
rpc_thread_num = static_cast<size_t>(config.rpc_thread_num);
|
||||
|
|
@ -120,6 +128,12 @@ class MasterServiceSupervisorConfig {
|
|||
put_start_release_timeout_sec = config.put_start_release_timeout_sec;
|
||||
enable_disk_eviction = config.enable_disk_eviction;
|
||||
quota_bytes = config.quota_bytes;
|
||||
max_replicas_per_key = config.max_replicas_per_key;
|
||||
if (config.deployment_mode == "Centralization") {
|
||||
deployment_mode = DeploymentMode::CENTRALIZATION;
|
||||
} else {
|
||||
deployment_mode = DeploymentMode::P2P;
|
||||
}
|
||||
|
||||
validate();
|
||||
}
|
||||
|
|
@ -155,6 +169,9 @@ class MasterServiceSupervisorConfig {
|
|||
if (!client_live_ttl_sec.IsSet()) {
|
||||
throw std::runtime_error("client_live_ttl_sec is not set");
|
||||
}
|
||||
if (!client_crashed_ttl_sec.IsSet()) {
|
||||
throw std::runtime_error("client_crashed_ttl_sec is not set");
|
||||
}
|
||||
if (!rpc_port.IsSet()) {
|
||||
throw std::runtime_error("rpc_port is not set");
|
||||
}
|
||||
|
|
@ -180,6 +197,7 @@ class WrappedMasterServiceConfig {
|
|||
DEFAULT_EVICTION_HIGH_WATERMARK_RATIO;
|
||||
ViewVersionId view_version = 0;
|
||||
int64_t client_live_ttl_sec = DEFAULT_CLIENT_LIVE_TTL_SEC;
|
||||
int64_t client_crashed_ttl_sec = DEFAULT_CLIENT_CRASHED_TTL_SEC;
|
||||
bool enable_ha = false;
|
||||
bool enable_offload = false;
|
||||
std::string cluster_id = DEFAULT_CLUSTER_ID;
|
||||
|
|
@ -190,6 +208,7 @@ class WrappedMasterServiceConfig {
|
|||
uint64_t put_start_release_timeout_sec = DEFAULT_PUT_START_RELEASE_TIMEOUT;
|
||||
bool enable_disk_eviction = true;
|
||||
uint64_t quota_bytes = 0;
|
||||
uint64_t max_replicas_per_key = 1;
|
||||
|
||||
WrappedMasterServiceConfig() = default;
|
||||
|
||||
|
|
@ -209,6 +228,7 @@ class WrappedMasterServiceConfig {
|
|||
eviction_high_watermark_ratio = config.eviction_high_watermark_ratio;
|
||||
view_version = view_version_param;
|
||||
client_live_ttl_sec = config.client_live_ttl_sec;
|
||||
client_crashed_ttl_sec = config.client_crashed_ttl_sec;
|
||||
enable_ha = config.enable_ha;
|
||||
enable_offload = config.enable_offload;
|
||||
cluster_id = config.cluster_id;
|
||||
|
|
@ -216,6 +236,7 @@ class WrappedMasterServiceConfig {
|
|||
global_file_segment_size = config.global_file_segment_size;
|
||||
enable_disk_eviction = config.enable_disk_eviction;
|
||||
quota_bytes = config.quota_bytes;
|
||||
max_replicas_per_key = config.max_replicas_per_key;
|
||||
|
||||
// Convert string memory_allocator to BufferAllocatorType enum
|
||||
if (config.memory_allocator == "cachelib") {
|
||||
|
|
@ -254,6 +275,7 @@ class WrappedMasterServiceConfig {
|
|||
memory_allocator = config.memory_allocator;
|
||||
enable_disk_eviction = config.enable_disk_eviction;
|
||||
quota_bytes = config.quota_bytes;
|
||||
max_replicas_per_key = config.max_replicas_per_key;
|
||||
put_start_discard_timeout_sec = config.put_start_discard_timeout_sec;
|
||||
put_start_release_timeout_sec = config.put_start_release_timeout_sec;
|
||||
}
|
||||
|
|
@ -274,6 +296,10 @@ class MasterServiceConfigBuilder {
|
|||
DEFAULT_EVICTION_HIGH_WATERMARK_RATIO;
|
||||
ViewVersionId view_version_ = 0;
|
||||
int64_t client_live_ttl_sec_ = DEFAULT_CLIENT_LIVE_TTL_SEC;
|
||||
// We use a separate flag to track if crashed ttl is explicitly set
|
||||
bool client_crashed_ttl_sec_set_ = false;
|
||||
int64_t client_crashed_ttl_sec_ = DEFAULT_CLIENT_CRASHED_TTL_SEC;
|
||||
|
||||
bool enable_ha_ = false;
|
||||
bool enable_offload_ = false;
|
||||
std::string cluster_id_ = DEFAULT_CLUSTER_ID;
|
||||
|
|
@ -282,6 +308,7 @@ class MasterServiceConfigBuilder {
|
|||
BufferAllocatorType memory_allocator_ = BufferAllocatorType::OFFSET;
|
||||
bool enable_disk_eviction_ = true;
|
||||
uint64_t quota_bytes_ = 0;
|
||||
uint64_t max_replicas_per_key_ = 1;
|
||||
uint64_t put_start_discard_timeout_sec_ = DEFAULT_PUT_START_DISCARD_TIMEOUT;
|
||||
uint64_t put_start_release_timeout_sec_ = DEFAULT_PUT_START_RELEASE_TIMEOUT;
|
||||
|
||||
|
|
@ -325,6 +352,12 @@ class MasterServiceConfigBuilder {
|
|||
return *this;
|
||||
}
|
||||
|
||||
MasterServiceConfigBuilder& set_client_crashed_ttl_sec(int64_t ttl) {
|
||||
client_crashed_ttl_sec_ = ttl;
|
||||
client_crashed_ttl_sec_set_ = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
MasterServiceConfigBuilder& set_enable_ha(bool enable) {
|
||||
enable_ha_ = enable;
|
||||
return *this;
|
||||
|
|
@ -357,6 +390,11 @@ class MasterServiceConfigBuilder {
|
|||
return *this;
|
||||
}
|
||||
|
||||
MasterServiceConfigBuilder& set_max_replicas_per_key(uint64_t limit) {
|
||||
max_replicas_per_key_ = limit;
|
||||
return *this;
|
||||
}
|
||||
|
||||
MasterServiceConfigBuilder& set_put_start_discard_timeout_sec(
|
||||
uint64_t put_start_discard_timeout_sec) {
|
||||
put_start_discard_timeout_sec_ = put_start_discard_timeout_sec;
|
||||
|
|
@ -383,6 +421,7 @@ class MasterServiceConfig {
|
|||
DEFAULT_EVICTION_HIGH_WATERMARK_RATIO;
|
||||
ViewVersionId view_version = 0;
|
||||
int64_t client_live_ttl_sec = DEFAULT_CLIENT_LIVE_TTL_SEC;
|
||||
int64_t client_crashed_ttl_sec = DEFAULT_CLIENT_CRASHED_TTL_SEC;
|
||||
bool enable_ha = false;
|
||||
bool enable_offload = false;
|
||||
std::string cluster_id = DEFAULT_CLUSTER_ID;
|
||||
|
|
@ -393,6 +432,7 @@ class MasterServiceConfig {
|
|||
uint64_t put_start_release_timeout_sec = DEFAULT_PUT_START_RELEASE_TIMEOUT;
|
||||
bool enable_disk_eviction = true;
|
||||
uint64_t quota_bytes = 0;
|
||||
uint64_t max_replicas_per_key = 1;
|
||||
|
||||
MasterServiceConfig() = default;
|
||||
|
||||
|
|
@ -406,6 +446,7 @@ class MasterServiceConfig {
|
|||
eviction_high_watermark_ratio = config.eviction_high_watermark_ratio;
|
||||
view_version = config.view_version;
|
||||
client_live_ttl_sec = config.client_live_ttl_sec;
|
||||
client_crashed_ttl_sec = config.client_crashed_ttl_sec;
|
||||
enable_ha = config.enable_ha;
|
||||
enable_offload = config.enable_offload;
|
||||
cluster_id = config.cluster_id;
|
||||
|
|
@ -414,6 +455,7 @@ class MasterServiceConfig {
|
|||
memory_allocator = config.memory_allocator;
|
||||
enable_disk_eviction = config.enable_disk_eviction;
|
||||
quota_bytes = config.quota_bytes;
|
||||
max_replicas_per_key = config.max_replicas_per_key;
|
||||
put_start_discard_timeout_sec = config.put_start_discard_timeout_sec;
|
||||
put_start_release_timeout_sec = config.put_start_release_timeout_sec;
|
||||
}
|
||||
|
|
@ -432,6 +474,7 @@ inline MasterServiceConfig MasterServiceConfigBuilder::build() const {
|
|||
config.eviction_high_watermark_ratio = eviction_high_watermark_ratio_;
|
||||
config.view_version = view_version_;
|
||||
config.client_live_ttl_sec = client_live_ttl_sec_;
|
||||
config.client_crashed_ttl_sec = client_crashed_ttl_sec_;
|
||||
config.enable_ha = enable_ha_;
|
||||
config.enable_offload = enable_offload_;
|
||||
config.cluster_id = cluster_id_;
|
||||
|
|
@ -442,6 +485,20 @@ inline MasterServiceConfig MasterServiceConfigBuilder::build() const {
|
|||
config.put_start_release_timeout_sec = put_start_release_timeout_sec_;
|
||||
config.enable_disk_eviction = enable_disk_eviction_;
|
||||
config.quota_bytes = quota_bytes_;
|
||||
config.max_replicas_per_key = max_replicas_per_key_;
|
||||
|
||||
// Logic for client_crashed_ttl_sec
|
||||
if (client_crashed_ttl_sec_set_) {
|
||||
// User explicitly set it, so we must validate it is >= live ttl
|
||||
if (config.client_crashed_ttl_sec < config.client_live_ttl_sec) {
|
||||
throw std::invalid_argument(
|
||||
"client_crashed_ttl_sec must be >= client_live_ttl_sec");
|
||||
}
|
||||
} else {
|
||||
// User did not set it, defaults to 3 * live ttl
|
||||
config.client_crashed_ttl_sec = config.client_live_ttl_sec * 3;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
@ -456,6 +513,8 @@ struct InProcMasterConfig {
|
|||
std::optional<int> http_metrics_port;
|
||||
std::optional<int> http_metadata_port;
|
||||
std::optional<uint64_t> default_kv_lease_ttl;
|
||||
std::optional<int64_t> client_live_ttl_sec;
|
||||
std::optional<int64_t> client_crashed_ttl_sec;
|
||||
};
|
||||
|
||||
// Builder class for InProcMasterConfig
|
||||
|
|
@ -465,6 +524,8 @@ class InProcMasterConfigBuilder {
|
|||
std::optional<int> http_metrics_port_ = std::nullopt;
|
||||
std::optional<int> http_metadata_port_ = std::nullopt;
|
||||
std::optional<uint64_t> default_kv_lease_ttl_ = std::nullopt;
|
||||
std::optional<int64_t> client_live_ttl_sec_ = std::nullopt;
|
||||
std::optional<int64_t> client_crashed_ttl_sec_ = std::nullopt;
|
||||
|
||||
public:
|
||||
InProcMasterConfigBuilder() = default;
|
||||
|
|
@ -489,6 +550,16 @@ class InProcMasterConfigBuilder {
|
|||
return *this;
|
||||
}
|
||||
|
||||
InProcMasterConfigBuilder& set_client_live_ttl_sec(int64_t ttl) {
|
||||
client_live_ttl_sec_ = ttl;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InProcMasterConfigBuilder& set_client_crashed_ttl_sec(int64_t ttl) {
|
||||
client_crashed_ttl_sec_ = ttl;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InProcMasterConfig build() const;
|
||||
};
|
||||
|
||||
|
|
@ -499,6 +570,8 @@ inline InProcMasterConfig InProcMasterConfigBuilder::build() const {
|
|||
config.http_metrics_port = http_metrics_port_;
|
||||
config.http_metadata_port = http_metadata_port_;
|
||||
config.default_kv_lease_ttl = default_kv_lease_ttl_;
|
||||
config.client_live_ttl_sec = client_live_ttl_sec_;
|
||||
config.client_crashed_ttl_sec = client_crashed_ttl_sec_;
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "ylt/metric/counter.hpp"
|
||||
|
|
@ -13,6 +12,7 @@ class MasterMetricManager {
|
|||
public:
|
||||
// --- Singleton Access ---
|
||||
static MasterMetricManager& instance();
|
||||
void reset_all_metrics();
|
||||
|
||||
MasterMetricManager(const MasterMetricManager&) = delete;
|
||||
MasterMetricManager& operator=(const MasterMetricManager&) = delete;
|
||||
|
|
@ -55,8 +55,6 @@ class MasterMetricManager {
|
|||
// Memory Storage Metrics
|
||||
void inc_allocated_mem_size(int64_t val = 1);
|
||||
void dec_allocated_mem_size(int64_t val = 1);
|
||||
void inc_total_mem_capacity(int64_t val = 1);
|
||||
void dec_total_mem_capacity(int64_t val = 1);
|
||||
int64_t get_allocated_mem_size();
|
||||
int64_t get_total_mem_capacity();
|
||||
double get_segment_mem_used_ratio(const std::string& segment);
|
||||
|
|
@ -111,8 +109,14 @@ class MasterMetricManager {
|
|||
void inc_unmount_segment_failures(int64_t val = 1);
|
||||
void inc_remount_segment_requests(int64_t val = 1);
|
||||
void inc_remount_segment_failures(int64_t val = 1);
|
||||
void inc_ping_requests(int64_t val = 1);
|
||||
void inc_ping_failures(int64_t val = 1);
|
||||
void inc_heartbeat_requests(int64_t val = 1);
|
||||
void inc_heartbeat_failures(int64_t val = 1);
|
||||
void inc_get_write_route_requests(int64_t val = 1);
|
||||
void inc_get_write_route_failures(int64_t val = 1);
|
||||
void inc_add_replica_requests(int64_t val = 1);
|
||||
void inc_add_replica_failures(int64_t val = 1);
|
||||
void inc_remove_replica_requests(int64_t val = 1);
|
||||
void inc_remove_replica_failures(int64_t val = 1);
|
||||
|
||||
// Batch Operation Statistics (Counters)
|
||||
void inc_batch_exist_key_requests(int64_t items);
|
||||
|
|
@ -136,6 +140,12 @@ class MasterMetricManager {
|
|||
void inc_batch_put_revoke_requests(int64_t items);
|
||||
void inc_batch_put_revoke_failures(int64_t failed_items);
|
||||
void inc_batch_put_revoke_partial_success(int64_t failed_items);
|
||||
void inc_batch_remove_replica_requests(int64_t items);
|
||||
void inc_batch_remove_replica_failures(int64_t failed_items);
|
||||
void inc_batch_remove_replica_partial_success(int64_t failed_items);
|
||||
void inc_batch_get_write_route_requests(int64_t items);
|
||||
void inc_batch_get_write_route_failures(int64_t failed_items);
|
||||
void inc_batch_get_write_route_partial_success(int64_t failed_items);
|
||||
|
||||
// Operation Statistics Getters
|
||||
int64_t get_put_start_requests();
|
||||
|
|
@ -162,8 +172,14 @@ class MasterMetricManager {
|
|||
int64_t get_unmount_segment_failures();
|
||||
int64_t get_remount_segment_requests();
|
||||
int64_t get_remount_segment_failures();
|
||||
int64_t get_ping_requests();
|
||||
int64_t get_ping_failures();
|
||||
int64_t get_heartbeat_requests();
|
||||
int64_t get_heartbeat_failures();
|
||||
int64_t get_get_write_route_requests();
|
||||
int64_t get_get_write_route_failures();
|
||||
int64_t get_add_replica_requests();
|
||||
int64_t get_add_replica_failures();
|
||||
int64_t get_remove_replica_requests();
|
||||
int64_t get_remove_replica_failures();
|
||||
|
||||
// Batch Operation Statistics Getters
|
||||
int64_t get_batch_exist_key_requests();
|
||||
|
|
@ -201,6 +217,14 @@ class MasterMetricManager {
|
|||
int64_t get_batch_put_revoke_partial_successes();
|
||||
int64_t get_batch_put_revoke_items();
|
||||
int64_t get_batch_put_revoke_failed_items();
|
||||
int64_t get_batch_remove_replica_requests();
|
||||
int64_t get_batch_remove_replica_failures();
|
||||
int64_t get_batch_remove_replica_partial_successes();
|
||||
int64_t get_batch_remove_replica_items();
|
||||
int64_t get_batch_remove_replica_failed_items();
|
||||
int64_t get_batch_get_write_route_requests();
|
||||
int64_t get_batch_get_write_route_failures();
|
||||
int64_t get_batch_get_write_route_partial_successes();
|
||||
|
||||
// Eviction Metrics
|
||||
void inc_eviction_success(int64_t key_count, int64_t size);
|
||||
|
|
@ -293,8 +317,14 @@ class MasterMetricManager {
|
|||
ylt::metric::counter_t unmount_segment_failures_;
|
||||
ylt::metric::counter_t remount_segment_requests_;
|
||||
ylt::metric::counter_t remount_segment_failures_;
|
||||
ylt::metric::counter_t ping_requests_;
|
||||
ylt::metric::counter_t ping_failures_;
|
||||
ylt::metric::counter_t heartbeat_requests_;
|
||||
ylt::metric::counter_t heartbeat_failures_;
|
||||
ylt::metric::counter_t get_write_route_requests_;
|
||||
ylt::metric::counter_t get_write_route_failures_;
|
||||
ylt::metric::counter_t add_replica_requests_;
|
||||
ylt::metric::counter_t add_replica_failures_;
|
||||
ylt::metric::counter_t remove_replica_requests_;
|
||||
ylt::metric::counter_t remove_replica_failures_;
|
||||
|
||||
// Batch Operation Statistics
|
||||
ylt::metric::counter_t batch_exist_key_requests_;
|
||||
|
|
@ -332,6 +362,14 @@ class MasterMetricManager {
|
|||
ylt::metric::counter_t batch_put_revoke_partial_successes_;
|
||||
ylt::metric::counter_t batch_put_revoke_items_;
|
||||
ylt::metric::counter_t batch_put_revoke_failed_items_;
|
||||
ylt::metric::counter_t batch_remove_replica_requests_;
|
||||
ylt::metric::counter_t batch_remove_replica_failures_;
|
||||
ylt::metric::counter_t batch_remove_replica_partial_successes_;
|
||||
ylt::metric::counter_t batch_remove_replica_items_;
|
||||
ylt::metric::counter_t batch_remove_replica_failed_items_;
|
||||
ylt::metric::counter_t batch_get_write_route_requests_;
|
||||
ylt::metric::counter_t batch_get_write_route_failures_;
|
||||
ylt::metric::counter_t batch_get_write_route_partial_successes_;
|
||||
|
||||
// cache hit Statistics
|
||||
ylt::metric::counter_t mem_cache_hit_nums_;
|
||||
|
|
|
|||
|
|
@ -1,80 +1,80 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <boost/lockfree/queue.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <ylt/util/expected.hpp>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
|
||||
#include "allocation_strategy.h"
|
||||
#include "master_metric_manager.h"
|
||||
#include "mutex.h"
|
||||
#include "segment.h"
|
||||
#include "types.h"
|
||||
#include "master_config.h"
|
||||
#include "rpc_types.h"
|
||||
#include "replica.h"
|
||||
#include "master_config.h"
|
||||
|
||||
namespace mooncake {
|
||||
// Forward declarations
|
||||
class AllocationStrategy;
|
||||
class EvictionStrategy;
|
||||
class ClientManager;
|
||||
|
||||
/*
|
||||
* @brief MasterService is the main class for the master server.
|
||||
* Lock order: To avoid deadlocks, the following lock order should be followed:
|
||||
* 1. client_mutex_
|
||||
* 2. metadata_shards_[shard_idx_].mutex
|
||||
* 3. segment_mutex_
|
||||
/**
|
||||
* @brief
|
||||
* 1. MasterService is a abstract base class for master server.
|
||||
* This class defines common rpc interfaces that correspond to
|
||||
* WrappedMasterService.
|
||||
*
|
||||
* 2. The multiple metadata of Master are hierarchical, MasterService manages
|
||||
* metadata of key (ObjectMeta), and its ClientManager manages metadata of
|
||||
* client (ClientMeta), each ClientMeta uses SegmentManager to manage its
|
||||
* segments. The relationship between metadata:
|
||||
* a. Client (1) —— (0..*) Segment
|
||||
* b. Key (1) —— (1..*) Replica
|
||||
* c. Replica (1) —— (1) Segment
|
||||
*
|
||||
* 3. The lock order of MasterService is:
|
||||
* a. MetadataShard's mutex
|
||||
* b. ClientManager's client_mutex_
|
||||
* c. SegmentManager's segment_mutex_
|
||||
* For avoiding deadlock, each metadata managers should follow this order.
|
||||
*/
|
||||
class MasterService {
|
||||
public:
|
||||
MasterService();
|
||||
MasterService(const MasterServiceConfig& config);
|
||||
~MasterService();
|
||||
virtual ~MasterService() = default;
|
||||
virtual void InitializeClientManager();
|
||||
|
||||
/**
|
||||
* @brief Mount a memory segment for buffer allocation. This function is
|
||||
* idempotent.
|
||||
* @return ErrorCode::OK on success,
|
||||
* ErrorCode::INVALID_PARAMS on invalid parameters,
|
||||
* ErrorCode::UNAVAILABLE_IN_CURRENT_STATUS if the segment cannot
|
||||
* be mounted temporarily,
|
||||
* ErrorCode::INTERNAL_ERROR on internal errors.
|
||||
* @brief Register a client with its segments.
|
||||
*/
|
||||
auto RegisterClient(const RegisterClientRequest& req)
|
||||
-> tl::expected<RegisterClientResponse, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief heartbeat interface for client to sync its status
|
||||
* @param req HeartbeatRequest containing client_id and tasks
|
||||
* @return HeartbeatResponse containing client status, view_version,
|
||||
* and task results
|
||||
*/
|
||||
auto Heartbeat(const HeartbeatRequest& req)
|
||||
-> tl::expected<HeartbeatResponse, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Queries the status of a client
|
||||
*/
|
||||
auto QueryClientStatus(const QueryClientStatusRequest& req)
|
||||
-> tl::expected<QueryClientStatusResponse, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Mount a memory segment.
|
||||
* @return ErrorCode::SEGMENT_ALREADY_EXISTS if it is already mounted.
|
||||
* ErrorCode::CLIENT_UNHEALTHY if the client is unhealthy.
|
||||
*/
|
||||
auto MountSegment(const Segment& segment, const UUID& client_id)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Re-mount segments, invoked when the client is the first time to
|
||||
* connect to the master or the client Ping TTL is expired and need
|
||||
* to remount. This function is idempotent. Client should retry if the
|
||||
* return code is not ErrorCode::OK.
|
||||
* @return ErrorCode::OK means either all segments are remounted
|
||||
* successfully or the fail is not solvable by a new remount request.
|
||||
* ErrorCode::UNAVAILABLE_IN_CURRENT_STATUS if the segment cannot
|
||||
* be mounted temporarily.
|
||||
* ErrorCode::INTERNAL_ERROR if something temporary error happens.
|
||||
*/
|
||||
auto ReMountSegment(const std::vector<Segment>& segments,
|
||||
const UUID& client_id) -> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Unmount a memory segment. This function is idempotent.
|
||||
* @brief Unmount a memory segment.
|
||||
* @return ErrorCode::OK on success,
|
||||
* ErrorCode::UNAVAILABLE_IN_CURRENT_STATUS if the segment is
|
||||
* currently unmounting.
|
||||
* ErrorCode::SEGMENT_NOT_FOUND if the segment doesn't exist
|
||||
* ErrorCode::CLIENT_UNHEALTHY if the client is unhealthy
|
||||
*/
|
||||
auto UnmountSegment(const UUID& segment_id, const UUID& client_id)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
|
@ -102,6 +102,15 @@ class MasterService {
|
|||
*/
|
||||
auto GetAllSegments() -> tl::expected<std::vector<std::string>, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Get all segments belonging to a specific client.
|
||||
* @param client_id The UUID of the client.
|
||||
* @return An expected object containing a vector of segment names on
|
||||
* success, or ErrorCode on failure.
|
||||
*/
|
||||
auto GetClientSegments(const UUID& client_id)
|
||||
-> tl::expected<std::vector<std::string>, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Query a segment's capacity and used size in bytes.
|
||||
* Conductor should use these information to schedule new requests.
|
||||
|
|
@ -132,22 +141,6 @@ class MasterService {
|
|||
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
|
||||
ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Batch clear KV cache replicas for specified object keys.
|
||||
* @param object_keys Vector of object key strings to clear.
|
||||
* @param client_id The UUID of the client that owns the object keys.
|
||||
* @param segment_name The name of the segment (storage device) to clear
|
||||
* from. If empty, clears replicas from all segments for the given
|
||||
* client_id.
|
||||
* @return An expected object containing a vector of successfully cleared
|
||||
* keys on success, or an ErrorCode on failure. Only successfully
|
||||
* cleared keys are included in the result.
|
||||
*/
|
||||
auto BatchReplicaClear(const std::vector<std::string>& object_keys,
|
||||
const UUID& client_id,
|
||||
const std::string& segment_name)
|
||||
-> tl::expected<std::vector<std::string>, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Retrieves replica lists for object keys that match a regex
|
||||
* pattern.
|
||||
|
|
@ -162,65 +155,16 @@ class MasterService {
|
|||
|
||||
/**
|
||||
* @brief Get list of replicas for an object
|
||||
* @param[out] replica_list Vector to store replica information
|
||||
* @return ErrorCode::OK on success, ErrorCode::REPLICA_IS_NOT_READY if not
|
||||
* ready
|
||||
* @param key The key of the object
|
||||
* @param config The filter configuration for the replica list
|
||||
* @return An expected object containing the replica list on success, or an
|
||||
* ErrorCode on failure.
|
||||
*/
|
||||
auto GetReplicaList(std::string_view key)
|
||||
virtual auto GetReplicaList(const std::string& key,
|
||||
const GetReplicaListRequestConfig& config =
|
||||
GetReplicaListRequestConfig())
|
||||
-> tl::expected<GetReplicaListResponse, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Start a put operation for an object
|
||||
* @param[out] replica_list Vector to store replica information for the
|
||||
* slice
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if exists,
|
||||
* ErrorCode::NO_AVAILABLE_HANDLE if allocation fails,
|
||||
* ErrorCode::INVALID_PARAMS if slice size is invalid
|
||||
*/
|
||||
auto PutStart(const UUID& client_id, const std::string& key,
|
||||
const uint64_t slice_length, const ReplicateConfig& config)
|
||||
-> tl::expected<std::vector<Replica::Descriptor>, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Complete a put operation, replica_type indicates the type of
|
||||
* replica to complete (memory or disk)
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* found, ErrorCode::INVALID_WRITE if replica status is invalid
|
||||
*/
|
||||
auto PutEnd(const UUID& client_id, const std::string& key,
|
||||
ReplicaType replica_type) -> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Adds a replica instance associated with the given client and key.
|
||||
*/
|
||||
auto AddReplica(const UUID& client_id, const std::string& key,
|
||||
Replica& replica) -> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Revoke a put operation, replica_type indicates the type of
|
||||
* replica to revoke (memory or disk)
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* found, ErrorCode::INVALID_WRITE if replica status is invalid
|
||||
*/
|
||||
auto PutRevoke(const UUID& client_id, const std::string& key,
|
||||
ReplicaType replica_type) -> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Complete a batch of put operations
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* found, ErrorCode::INVALID_WRITE if replica status is invalid
|
||||
*/
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
|
||||
const UUID& client_id, const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief Revoke a batch of put operations
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
* found, ErrorCode::INVALID_WRITE if replica status is invalid
|
||||
*/
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
|
||||
const UUID& client_id, const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief Remove an object and its replicas
|
||||
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
|
||||
|
|
@ -248,428 +192,204 @@ class MasterService {
|
|||
*/
|
||||
size_t GetKeyCount() const;
|
||||
|
||||
/**
|
||||
* @brief Heartbeat from client
|
||||
* @param client_id The uuid of the client
|
||||
* @return PingResponse containing view version and client status
|
||||
* @return ErrorCode::OK on success, ErrorCode::INTERNAL_ERROR if the client
|
||||
* ping queue is full
|
||||
*/
|
||||
auto Ping(const UUID& client_id) -> tl::expected<PingResponse, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Get the master service cluster ID to use as subdirectory name
|
||||
* @return ErrorCode::OK on success, ErrorCode::INTERNAL_ERROR if cluster ID
|
||||
* is not set
|
||||
*/
|
||||
tl::expected<std::string, ErrorCode> GetFsdir() const;
|
||||
|
||||
/**
|
||||
* @brief Get storage backend configuration including eviction settings
|
||||
* @return GetStorageConfigResponse containing fsdir, enable_disk_eviction,
|
||||
* and quota_bytes
|
||||
*/
|
||||
tl::expected<GetStorageConfigResponse, ErrorCode> GetStorageConfig() const;
|
||||
|
||||
/**
|
||||
* @brief Mounts a file storage segment into the master.
|
||||
* @param enable_offloading If true, enables offloading (write-to-file).
|
||||
*/
|
||||
auto MountLocalDiskSegment(const UUID& client_id, bool enable_offloading)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Heartbeat call to collect object-level statistics and retrieve the
|
||||
* set of non-offloaded objects.
|
||||
* @param enable_offloading Indicates whether offloading is enabled for this
|
||||
* segment.
|
||||
*/
|
||||
auto OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading)
|
||||
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Notifies the master that offloading of specified objects has
|
||||
* succeeded.
|
||||
* @param keys A list of object keys (names) that were successfully
|
||||
* offloaded.
|
||||
* @param metadatas The corresponding metadata for each offloaded object,
|
||||
* including size, storage location, etc.
|
||||
*/
|
||||
auto NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
private:
|
||||
// Resolve the key to a sanitized format for storage
|
||||
std::string SanitizeKey(const std::string& key) const;
|
||||
std::string ResolvePath(const std::string& key) const;
|
||||
|
||||
// BatchEvict evicts objects in a near-LRU way, i.e., prioritizes to evict
|
||||
// object with smaller lease timeout. It has two passes. The first pass only
|
||||
// evicts objects without soft pin. The second pass prioritizes objects
|
||||
// without soft pin, but also allows to evict soft pinned objects if
|
||||
// allow_evict_soft_pinned_objects_ is true. The first pass tries fulfill
|
||||
// evict ratio target. If the actual evicted ratio is less than
|
||||
// evict_ratio_lowerbound, the second pass will be triggered and try to
|
||||
// fulfill evict ratio lowerbound.
|
||||
void BatchEvict(double evict_ratio_target, double evict_ratio_lowerbound);
|
||||
|
||||
// Clear invalid handles in all shards
|
||||
void ClearInvalidHandles();
|
||||
|
||||
// Internal data structures
|
||||
protected:
|
||||
struct ObjectMetadata {
|
||||
// RAII-style metric management
|
||||
~ObjectMetadata() {
|
||||
MasterMetricManager::instance().dec_key_count(1);
|
||||
if (soft_pin_timeout) {
|
||||
MasterMetricManager::instance().dec_soft_pin_key_count(1);
|
||||
}
|
||||
}
|
||||
public:
|
||||
virtual ~ObjectMetadata();
|
||||
|
||||
ObjectMetadata(size_t value_length, std::vector<Replica>&& reps);
|
||||
ObjectMetadata() = delete;
|
||||
|
||||
ObjectMetadata(
|
||||
const UUID& client_id_,
|
||||
const std::chrono::steady_clock::time_point put_start_time_,
|
||||
size_t value_length, std::vector<Replica>&& reps,
|
||||
bool enable_soft_pin)
|
||||
: client_id(client_id_),
|
||||
put_start_time(put_start_time_),
|
||||
replicas(std::move(reps)),
|
||||
size(value_length),
|
||||
lease_timeout(),
|
||||
soft_pin_timeout(std::nullopt) {
|
||||
MasterMetricManager::instance().inc_key_count(1);
|
||||
if (enable_soft_pin) {
|
||||
soft_pin_timeout.emplace();
|
||||
MasterMetricManager::instance().inc_soft_pin_key_count(1);
|
||||
}
|
||||
MasterMetricManager::instance().observe_value_size(value_length);
|
||||
}
|
||||
|
||||
ObjectMetadata(const ObjectMetadata&) = delete;
|
||||
ObjectMetadata& operator=(const ObjectMetadata&) = delete;
|
||||
ObjectMetadata(ObjectMetadata&&) = delete;
|
||||
ObjectMetadata& operator=(ObjectMetadata&&) = delete;
|
||||
|
||||
const UUID client_id;
|
||||
const std::chrono::steady_clock::time_point put_start_time;
|
||||
// Check if the metadata is valid
|
||||
// Valid means it has at least one replica and size is greater than 0
|
||||
bool IsValid() const { return !replicas_.empty() && size_ > 0; }
|
||||
|
||||
std::vector<Replica> replicas;
|
||||
size_t size;
|
||||
// Default constructor, creates a time_point representing
|
||||
// the Clock's epoch (i.e., time_since_epoch() is zero).
|
||||
std::chrono::steady_clock::time_point lease_timeout; // hard lease
|
||||
std::optional<std::chrono::steady_clock::time_point>
|
||||
soft_pin_timeout; // optional soft pin, only set for vip objects
|
||||
public:
|
||||
// Attention:
|
||||
// The MasterService instance will call hook functions based on
|
||||
// following status functions. Each subclass of ObjectMetadata should
|
||||
// define its own status by overriding these functions.
|
||||
|
||||
// Check if there are some replicas with a different status than the
|
||||
// given value. If there are, return the status of the first replica
|
||||
// that is not equal to the given value. Otherwise, return false.
|
||||
std::optional<ReplicaStatus> HasDiffRepStatus(
|
||||
ReplicaStatus status, ReplicaType replica_type) const {
|
||||
for (const auto& replica : replicas) {
|
||||
if (replica.status() != status &&
|
||||
replica.type() == replica_type) {
|
||||
return replica.status();
|
||||
/**
|
||||
* @brief Whether the object is readable
|
||||
* @return true if the object is readable, false otherwise
|
||||
*/
|
||||
virtual bool IsObjectAccessible() const {
|
||||
for (const auto& replica : replicas_) {
|
||||
if (IsReplicaAccessible(replica)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the object is removable
|
||||
* @return ErrorCode::OK if removable, otherwise return error specific
|
||||
* to the reason
|
||||
*/
|
||||
virtual tl::expected<void, ErrorCode> IsObjectRemovable() const {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Grant a lease with timeout as now() + ttl, only update if the new
|
||||
// timeout is larger
|
||||
void GrantLease(const uint64_t ttl, const uint64_t soft_ttl) {
|
||||
std::chrono::steady_clock::time_point now =
|
||||
std::chrono::steady_clock::now();
|
||||
lease_timeout =
|
||||
std::max(lease_timeout, now + std::chrono::milliseconds(ttl));
|
||||
if (soft_pin_timeout) {
|
||||
soft_pin_timeout =
|
||||
std::max(*soft_pin_timeout,
|
||||
now + std::chrono::milliseconds(soft_ttl));
|
||||
}
|
||||
/**
|
||||
* @brief Whether the replica is readable
|
||||
* @return true if the replica is readable, false otherwise
|
||||
*/
|
||||
virtual bool IsReplicaAccessible(const Replica& replica) const {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Whether the replica is removable
|
||||
* @return ErrorCode::OK if removable, otherwise return error specific
|
||||
* to the reason
|
||||
*/
|
||||
virtual tl::expected<void, ErrorCode> IsReplicaRemovable(
|
||||
const Replica& replica) const {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Erase all replicas of the given type
|
||||
void EraseReplica(ReplicaType replica_type) {
|
||||
replicas.erase(
|
||||
std::remove_if(replicas.begin(), replicas.end(),
|
||||
[replica_type](const Replica& replica) {
|
||||
return replica.type() == replica_type;
|
||||
}),
|
||||
replicas.end());
|
||||
}
|
||||
|
||||
// Check if there is a memory replica
|
||||
bool HasMemReplica() const {
|
||||
return std::any_of(replicas.begin(), replicas.end(),
|
||||
[](const Replica& replica) {
|
||||
return replica.type() == ReplicaType::MEMORY;
|
||||
});
|
||||
}
|
||||
|
||||
// Get the count of memory replicas
|
||||
int GetMemReplicaCount() const {
|
||||
return std::count_if(
|
||||
replicas.begin(), replicas.end(), [](const Replica& replica) {
|
||||
return replica.type() == ReplicaType::MEMORY;
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the lease has expired
|
||||
bool IsLeaseExpired() const {
|
||||
return std::chrono::steady_clock::now() >= lease_timeout;
|
||||
}
|
||||
|
||||
// Check if the lease has expired
|
||||
bool IsLeaseExpired(std::chrono::steady_clock::time_point& now) const {
|
||||
return now >= lease_timeout;
|
||||
}
|
||||
|
||||
// Check if is in soft pin status
|
||||
bool IsSoftPinned() const {
|
||||
return soft_pin_timeout &&
|
||||
std::chrono::steady_clock::now() < *soft_pin_timeout;
|
||||
}
|
||||
|
||||
// Check if is in soft pin status
|
||||
bool IsSoftPinned(std::chrono::steady_clock::time_point& now) const {
|
||||
return soft_pin_timeout && now < *soft_pin_timeout;
|
||||
}
|
||||
|
||||
// Check if the metadata is valid
|
||||
// Valid means it has at least one replica and size is greater than 0
|
||||
bool IsValid() const { return !replicas.empty() && size > 0; }
|
||||
|
||||
bool IsAllReplicasComplete() const {
|
||||
return std::all_of(
|
||||
replicas.begin(), replicas.end(), [](const Replica& replica) {
|
||||
return replica.status() == ReplicaStatus::COMPLETE;
|
||||
});
|
||||
}
|
||||
|
||||
bool HasCompletedReplicas() const {
|
||||
return std::any_of(
|
||||
replicas.begin(), replicas.end(), [](const Replica& replica) {
|
||||
return replica.status() == ReplicaStatus::COMPLETE;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<Replica> DiscardProcessingReplicas() {
|
||||
auto partition_point = std::partition(
|
||||
replicas.begin(), replicas.end(), [](const Replica& replica) {
|
||||
return replica.status() != ReplicaStatus::PROCESSING;
|
||||
});
|
||||
|
||||
std::vector<Replica> discarded_replicas;
|
||||
if (partition_point != replicas.end()) {
|
||||
discarded_replicas.reserve(
|
||||
std::distance(partition_point, replicas.end()));
|
||||
std::move(partition_point, replicas.end(),
|
||||
std::back_inserter(discarded_replicas));
|
||||
replicas.erase(partition_point, replicas.end());
|
||||
}
|
||||
|
||||
return discarded_replicas;
|
||||
}
|
||||
public:
|
||||
std::vector<Replica> replicas_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
static constexpr size_t kNumShards = 1024; // Number of metadata shards
|
||||
|
||||
// Sharded metadata maps and their mutexes
|
||||
protected:
|
||||
// Sharded metadata maps and their mutexes.
|
||||
// Attention:
|
||||
// 1. Each subclass of MasterService should define its own shard and provide
|
||||
// the accessor functions.
|
||||
// 2. `segment_key_index` is a reverse index for `metadata` and segment.
|
||||
// Due to the object key in `segment_key_index` is a string_view acquired
|
||||
// from `metadata`, when removing an entry from `metadata`, you MUST
|
||||
// first remove the corresponding key from `segment_key_index`.
|
||||
struct MetadataShard {
|
||||
mutable Mutex mutex;
|
||||
std::unordered_map<std::string, ObjectMetadata> metadata
|
||||
GUARDED_BY(mutex);
|
||||
std::unordered_set<std::string> processing_keys GUARDED_BY(mutex);
|
||||
std::unordered_map<std::string, std::unique_ptr<ObjectMetadata>>
|
||||
metadata GUARDED_BY(mutex);
|
||||
|
||||
// segment_id -> { key -> replica_reference_count }.
|
||||
std::unordered_map<UUID, std::unordered_map<std::string_view, size_t>,
|
||||
boost::hash<UUID>>
|
||||
segment_key_index GUARDED_BY(mutex);
|
||||
};
|
||||
std::array<MetadataShard, kNumShards> metadata_shards_;
|
||||
// Virtual function to access shards
|
||||
virtual MetadataShard& GetShard(size_t idx) = 0;
|
||||
virtual const MetadataShard& GetShard(size_t idx) const = 0;
|
||||
virtual size_t GetShardIndex(const std::string& key) const = 0;
|
||||
virtual size_t GetShardCount() const = 0;
|
||||
|
||||
// Helper to get shard index from key
|
||||
size_t getShardIndex(const std::string& key) const {
|
||||
return std::hash<std::string>{}(key) % kNumShards;
|
||||
}
|
||||
// Helpers for maintaining per-shard segment_key_index.
|
||||
// 1. Must be called while holding shard.mutex.
|
||||
// 2. When add or remove a replica, must call the following functions to
|
||||
// update the segment_key_index.
|
||||
void AddReplicaToSegmentIndex(MetadataShard& shard, const std::string& key,
|
||||
const Replica& replica)
|
||||
NO_THREAD_SAFETY_ANALYSIS;
|
||||
void RemoveReplicaFromSegmentIndex(
|
||||
MetadataShard& shard, const std::string& key,
|
||||
const std::vector<Replica>& replicas) NO_THREAD_SAFETY_ANALYSIS;
|
||||
void RemoveReplicaFromSegmentIndex(
|
||||
MetadataShard& shard, const std::string& key,
|
||||
const Replica& replica) NO_THREAD_SAFETY_ANALYSIS;
|
||||
|
||||
// Helper to clean up stale handles pointing to unmounted segments
|
||||
bool CleanupStaleHandles(ObjectMetadata& metadata);
|
||||
|
||||
/**
|
||||
* @brief Helper to discard expired processing keys.
|
||||
*/
|
||||
void DiscardExpiredProcessingKeys(
|
||||
MetadataShard& shard, const std::chrono::steady_clock::time_point& now);
|
||||
|
||||
/**
|
||||
* @brief Helper to release space of expired discarded replicas.
|
||||
* @return Number of released objects that have memory replicas
|
||||
*/
|
||||
uint64_t ReleaseExpiredDiscardedReplicas(
|
||||
const std::chrono::steady_clock::time_point& now);
|
||||
|
||||
// Eviction thread function
|
||||
void EvictionThreadFunc();
|
||||
|
||||
tl::expected<void, ErrorCode> PushOffloadingQueue(const std::string& key,
|
||||
const Replica& replica);
|
||||
|
||||
// Lease related members
|
||||
const uint64_t default_kv_lease_ttl_; // in milliseconds
|
||||
const uint64_t default_kv_soft_pin_ttl_; // in milliseconds
|
||||
const bool allow_evict_soft_pinned_objects_;
|
||||
|
||||
// Eviction related members
|
||||
std::atomic<bool> need_eviction_{
|
||||
false}; // Set to trigger eviction when not enough space left
|
||||
const double eviction_ratio_; // in range [0.0, 1.0]
|
||||
const double eviction_high_watermark_ratio_; // in range [0.0, 1.0]
|
||||
|
||||
// Eviction thread related members
|
||||
std::thread eviction_thread_;
|
||||
std::atomic<bool> eviction_running_{false};
|
||||
static constexpr uint64_t kEvictionThreadSleepMs =
|
||||
10; // 10 ms sleep between eviction checks
|
||||
|
||||
// Helper class for accessing metadata with automatic locking and cleanup
|
||||
protected:
|
||||
// Helper class for accessing metadata with automatic locking
|
||||
class MetadataAccessor {
|
||||
public:
|
||||
MetadataAccessor(MasterService* service, const std::string& key)
|
||||
: service_(service),
|
||||
key_(key),
|
||||
shard_idx_(service_->getShardIndex(key)),
|
||||
shard_(service_->metadata_shards_[shard_idx_]),
|
||||
shard_idx_(service_->GetShardIndex(key)),
|
||||
shard_(service_->GetShard(shard_idx_)),
|
||||
lock_(&shard_.mutex),
|
||||
it_(shard_.metadata.find(key)),
|
||||
processing_it_(shard_.processing_keys.find(key)) {
|
||||
// Automatically clean up invalid handles
|
||||
if (it_ != shard_.metadata.end()) {
|
||||
if (service_->CleanupStaleHandles(it_->second)) {
|
||||
this->Erase();
|
||||
it_(shard_.metadata.find(key)) {}
|
||||
|
||||
if (processing_it_ != shard_.processing_keys.end()) {
|
||||
this->EraseFromProcessing();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
virtual ~MetadataAccessor() = default;
|
||||
|
||||
// Check if metadata exists
|
||||
bool Exists() const NO_THREAD_SAFETY_ANALYSIS {
|
||||
return it_ != shard_.metadata.end();
|
||||
}
|
||||
|
||||
bool InProcessing() const NO_THREAD_SAFETY_ANALYSIS {
|
||||
return processing_it_ != shard_.processing_keys.end();
|
||||
MetadataShard& GetShard() NO_THREAD_SAFETY_ANALYSIS { return shard_; }
|
||||
|
||||
const std::string& GetKey() const NO_THREAD_SAFETY_ANALYSIS {
|
||||
return it_->first;
|
||||
}
|
||||
|
||||
// Get metadata (only call when Exists() is true)
|
||||
ObjectMetadata& Get() NO_THREAD_SAFETY_ANALYSIS { return it_->second; }
|
||||
ObjectMetadata& Get() NO_THREAD_SAFETY_ANALYSIS { return *it_->second; }
|
||||
|
||||
// Delete current metadata (for PutRevoke or Remove operations)
|
||||
// Delete current metadata.
|
||||
// To prevent dangling string_views in segment_key_index, segment index
|
||||
// should be cleaned up before erasing the metadata entry.
|
||||
void Erase() NO_THREAD_SAFETY_ANALYSIS {
|
||||
shard_.metadata.erase(it_);
|
||||
it_ = shard_.metadata.end();
|
||||
if (it_ != shard_.metadata.end()) {
|
||||
service_->RemoveReplicaFromSegmentIndex(shard_, it_->first,
|
||||
it_->second->replicas_);
|
||||
shard_.metadata.erase(it_);
|
||||
it_ = shard_.metadata.end();
|
||||
}
|
||||
}
|
||||
|
||||
void EraseFromProcessing() NO_THREAD_SAFETY_ANALYSIS {
|
||||
shard_.processing_keys.erase(processing_it_);
|
||||
processing_it_ = shard_.processing_keys.end();
|
||||
}
|
||||
|
||||
private:
|
||||
protected:
|
||||
MasterService* service_;
|
||||
std::string key_;
|
||||
size_t shard_idx_;
|
||||
MetadataShard& shard_;
|
||||
MutexLocker lock_;
|
||||
std::unordered_map<std::string, ObjectMetadata>::iterator it_;
|
||||
std::unordered_set<std::string>::iterator processing_it_;
|
||||
std::unordered_map<std::string,
|
||||
std::unique_ptr<ObjectMetadata>>::iterator it_;
|
||||
};
|
||||
|
||||
friend class MetadataAccessor;
|
||||
virtual std::unique_ptr<MetadataAccessor> GetMetadataAccessor(
|
||||
const std::string& key) {
|
||||
return std::make_unique<MetadataAccessor>(this, key);
|
||||
}
|
||||
|
||||
ViewVersionId view_version_;
|
||||
protected:
|
||||
virtual ClientManager& GetClientManager() = 0;
|
||||
virtual const ClientManager& GetClientManager() const = 0;
|
||||
|
||||
// Client related members
|
||||
mutable std::shared_mutex client_mutex_;
|
||||
std::unordered_set<UUID, boost::hash<UUID>>
|
||||
ok_client_; // client with ok status
|
||||
void ClientMonitorFunc();
|
||||
std::thread client_monitor_thread_;
|
||||
std::atomic<bool> client_monitor_running_{false};
|
||||
static constexpr uint64_t kClientMonitorSleepMs =
|
||||
1000; // 1000 ms sleep between client monitor checks
|
||||
// boost lockfree queue requires trivial assignment operator
|
||||
struct PodUUID {
|
||||
uint64_t first;
|
||||
uint64_t second;
|
||||
};
|
||||
static constexpr size_t kClientPingQueueSize =
|
||||
128 * 1024; // Size of the client ping queue
|
||||
boost::lockfree::queue<PodUUID> client_ping_queue_{kClientPingQueueSize};
|
||||
const int64_t client_live_ttl_sec_;
|
||||
protected:
|
||||
virtual std::vector<Replica::Descriptor> FilterReplicas(
|
||||
const GetReplicaListRequestConfig& config,
|
||||
const ObjectMetadata& metadata) = 0;
|
||||
|
||||
// The following methods are hooks function to handle special events
|
||||
|
||||
// Triggered when the metadata of an object is accessed (e.g. Get or Exist)
|
||||
virtual void OnObjectAccessed(ObjectMetadata& metadata) = 0;
|
||||
|
||||
// Triggered when the object is removed
|
||||
virtual void OnObjectRemoved(ObjectMetadata& metadata);
|
||||
|
||||
// Triggered when the object is hit (e.g. Get)
|
||||
virtual void OnObjectHit(const ObjectMetadata& metadata) = 0;
|
||||
|
||||
// Triggered when the replica is removed
|
||||
virtual void OnReplicaRemoved(const Replica& replica) = 0;
|
||||
|
||||
// Triggered when the replica is added
|
||||
virtual void OnReplicaAdded(const Replica& replica) = 0;
|
||||
|
||||
// Callback for segment removal (triggered by ClientManager via
|
||||
// SegmentManager)
|
||||
virtual void OnSegmentRemoved(const UUID& segment_id);
|
||||
|
||||
protected:
|
||||
// if high availability features enabled
|
||||
const bool enable_ha_;
|
||||
ViewVersionId view_version_;
|
||||
|
||||
const bool enable_offload_;
|
||||
|
||||
// cluster id for persistent sub directory
|
||||
const std::string cluster_id_;
|
||||
// root filesystem directory for persistent storage
|
||||
const std::string root_fs_dir_;
|
||||
// global 3fs/nfs segment size
|
||||
int64_t global_file_segment_size_;
|
||||
// storage backend eviction configuration
|
||||
const bool enable_disk_eviction_;
|
||||
const uint64_t quota_bytes_;
|
||||
|
||||
bool use_disk_replica_{false};
|
||||
|
||||
// Segment management
|
||||
SegmentManager segment_manager_;
|
||||
BufferAllocatorType memory_allocator_type_;
|
||||
std::shared_ptr<AllocationStrategy> allocation_strategy_;
|
||||
|
||||
// Discarded replicas management
|
||||
const std::chrono::seconds put_start_discard_timeout_sec_;
|
||||
const std::chrono::seconds put_start_release_timeout_sec_;
|
||||
class DiscardedReplicas {
|
||||
public:
|
||||
DiscardedReplicas() = delete;
|
||||
|
||||
DiscardedReplicas(std::vector<Replica>&& replicas,
|
||||
std::chrono::steady_clock::time_point ttl)
|
||||
: replicas_(std::move(replicas)), ttl_(ttl), mem_size_(0) {
|
||||
for (auto& replica : replicas_) {
|
||||
mem_size_ += replica.get_memory_buffer_size();
|
||||
}
|
||||
MasterMetricManager::instance().inc_put_start_discard_cnt(
|
||||
1, mem_size_);
|
||||
}
|
||||
|
||||
~DiscardedReplicas() {
|
||||
MasterMetricManager::instance().inc_put_start_release_cnt(
|
||||
1, mem_size_);
|
||||
}
|
||||
|
||||
uint64_t memSize() const { return mem_size_; }
|
||||
|
||||
bool isExpired(const std::chrono::steady_clock::time_point& now) const {
|
||||
return ttl_ <= now;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Replica> replicas_;
|
||||
std::chrono::steady_clock::time_point ttl_;
|
||||
uint64_t mem_size_;
|
||||
};
|
||||
std::mutex discarded_replicas_mutex_;
|
||||
std::list<DiscardedReplicas> discarded_replicas_
|
||||
GUARDED_BY(discarded_replicas_mutex_);
|
||||
size_t offloading_queue_limit_ = 50000;
|
||||
friend class MetadataAccessor;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <atomic>
|
||||
|
||||
// Enable thread safety attributes only with clang.
|
||||
// The attributes can be safely erased when compiling with other compilers.
|
||||
|
|
@ -56,6 +57,15 @@
|
|||
#define NO_THREAD_SAFETY_ANALYSIS \
|
||||
THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)
|
||||
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
#define MOONCAKE_CPU_RELAX() asm volatile("pause" ::: "memory")
|
||||
#elif defined(__aarch64__)
|
||||
#define MOONCAKE_CPU_RELAX() asm volatile("yield" ::: "memory")
|
||||
#else
|
||||
#include <thread>
|
||||
#define MOONCAKE_CPU_RELAX() std::this_thread::yield()
|
||||
#endif
|
||||
|
||||
// Simple mutex implementation using std::mutex for exclusive locking only.
|
||||
class CAPABILITY("mutex") Mutex {
|
||||
private:
|
||||
|
|
@ -106,6 +116,84 @@ class CAPABILITY("shared_mutex") SharedMutex {
|
|||
const SharedMutex& operator!() const { return *this; }
|
||||
};
|
||||
|
||||
// Simple spinlock implementation using std::atomic<bool>.
|
||||
class CAPABILITY("mutex") SpinLock {
|
||||
private:
|
||||
std::atomic<bool> flag_{false};
|
||||
|
||||
public:
|
||||
void lock() ACQUIRE() {
|
||||
while (flag_.exchange(true, std::memory_order_acquire)) {
|
||||
while (flag_.load(std::memory_order_relaxed)) {
|
||||
MOONCAKE_CPU_RELAX();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void unlock() RELEASE() { flag_.store(false, std::memory_order_release); }
|
||||
|
||||
bool try_lock() TRY_ACQUIRE(true) {
|
||||
return !flag_.exchange(true, std::memory_order_acquire);
|
||||
}
|
||||
|
||||
const SpinLock& operator!() const { return *this; }
|
||||
};
|
||||
|
||||
// Simple spin-read-write lock implementation.
|
||||
// state_ > 0: number of readers
|
||||
// state_ == -1: writer
|
||||
class CAPABILITY("shared_mutex") SpinRWLock {
|
||||
public:
|
||||
void lock() ACQUIRE() {
|
||||
int32_t expected = 0;
|
||||
while (!state_.compare_exchange_weak(expected, -1,
|
||||
std::memory_order_acquire)) {
|
||||
while (state_.load(std::memory_order_relaxed) != 0) {
|
||||
MOONCAKE_CPU_RELAX();
|
||||
}
|
||||
expected = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void lock_shared() ACQUIRE_SHARED() {
|
||||
while (true) {
|
||||
int32_t current = state_.load(std::memory_order_relaxed);
|
||||
if (current >= 0) {
|
||||
if (state_.compare_exchange_weak(current, current + 1,
|
||||
std::memory_order_acquire)) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
MOONCAKE_CPU_RELAX();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void unlock() RELEASE() { state_.store(0, std::memory_order_release); }
|
||||
|
||||
void unlock_shared() RELEASE_SHARED() {
|
||||
state_.fetch_sub(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool try_lock() TRY_ACQUIRE(true) {
|
||||
int32_t expected = 0;
|
||||
return state_.compare_exchange_strong(expected, -1,
|
||||
std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool try_lock_shared() TRY_ACQUIRE_SHARED(true) {
|
||||
int32_t current = state_.load(std::memory_order_relaxed);
|
||||
if (current < 0) return false;
|
||||
return state_.compare_exchange_strong(current, current + 1,
|
||||
std::memory_order_acquire);
|
||||
}
|
||||
|
||||
const SpinRWLock& operator!() const { return *this; }
|
||||
|
||||
private:
|
||||
std::atomic<int32_t> state_{0};
|
||||
};
|
||||
|
||||
// MutexLocker is an RAII class that acquires a mutex in its constructor, and
|
||||
// releases it in its destructor.
|
||||
class SCOPED_CAPABILITY MutexLocker {
|
||||
|
|
@ -117,6 +205,14 @@ class SCOPED_CAPABILITY MutexLocker {
|
|||
// Acquire mu, implicitly acquire *this and associate it with mu.
|
||||
MutexLocker(Mutex* mu) ACQUIRE(mu) : mut(mu), locked(true) { mu->lock(); }
|
||||
|
||||
// Constructor without immediate locking.
|
||||
MutexLocker(Mutex* mu, bool lock_now) : mut(mu), locked(false) {
|
||||
if (lock_now) {
|
||||
mut->lock();
|
||||
locked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Release *this and all associated mutexes, if they are still held.
|
||||
~MutexLocker() RELEASE() {
|
||||
if (locked) {
|
||||
|
|
@ -228,4 +324,61 @@ class SCOPED_CAPABILITY SharedMutexLocker {
|
|||
}
|
||||
};
|
||||
|
||||
// RAII class for SpinLock
|
||||
class SCOPED_CAPABILITY SpinLockLocker {
|
||||
private:
|
||||
SpinLock* mut;
|
||||
bool locked;
|
||||
|
||||
public:
|
||||
explicit SpinLockLocker(SpinLock* mu) ACQUIRE(mu) : mut(mu), locked(true) {
|
||||
mu->lock();
|
||||
}
|
||||
~SpinLockLocker() RELEASE() {
|
||||
if (locked) mut->unlock();
|
||||
}
|
||||
|
||||
// Prevent copying and assignment
|
||||
SpinLockLocker(const SpinLockLocker&) = delete;
|
||||
SpinLockLocker& operator=(const SpinLockLocker&) = delete;
|
||||
|
||||
void unlock() RELEASE() {
|
||||
if (!locked) return;
|
||||
mut->unlock();
|
||||
locked = false;
|
||||
}
|
||||
};
|
||||
|
||||
// RAII class for SpinRWLock
|
||||
class SCOPED_CAPABILITY SpinRWLockLocker {
|
||||
private:
|
||||
SpinRWLock* mut;
|
||||
bool is_exclusive;
|
||||
bool locked;
|
||||
|
||||
public:
|
||||
explicit SpinRWLockLocker(SpinRWLock* mu) ACQUIRE(mu)
|
||||
: mut(mu), is_exclusive(true), locked(true) {
|
||||
mu->lock();
|
||||
}
|
||||
SpinRWLockLocker(SpinRWLock* mu, const shared_lock_t&) ACQUIRE_SHARED(mu)
|
||||
: mut(mu), is_exclusive(false), locked(true) {
|
||||
mu->lock_shared();
|
||||
}
|
||||
~SpinRWLockLocker() RELEASE() { unlock(); }
|
||||
|
||||
// Prevent copying and assignment
|
||||
SpinRWLockLocker(const SpinRWLockLocker&) = delete;
|
||||
SpinRWLockLocker& operator=(const SpinRWLockLocker&) = delete;
|
||||
|
||||
void unlock() RELEASE() {
|
||||
if (!locked) return;
|
||||
if (is_exclusive)
|
||||
mut->unlock();
|
||||
else
|
||||
mut->unlock_shared();
|
||||
locked = false;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // THREAD_SAFETY_ANALYSIS_MUTEX_H
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_manager.h"
|
||||
#include "p2p_client_meta.h"
|
||||
|
||||
namespace mooncake {
|
||||
class P2PClientManager final : public ClientManager {
|
||||
public:
|
||||
P2PClientManager(const int64_t disconnect_timeout_sec,
|
||||
const int64_t crash_timeout_sec,
|
||||
const ViewVersionId view_version);
|
||||
|
||||
protected:
|
||||
DeploymentMode GetDeploymentMode() const override {
|
||||
return DeploymentMode::P2P;
|
||||
}
|
||||
|
||||
std::unique_ptr<ClientIterator> InnerBuildClientIterator(
|
||||
ObjectIterateStrategy strategy) override;
|
||||
std::shared_ptr<ClientMeta> CreateClientMeta(
|
||||
const RegisterClientRequest& req) override;
|
||||
|
||||
void OnClientRegistered(const std::shared_ptr<ClientMeta>& meta) override {
|
||||
auto p2p_meta = std::dynamic_pointer_cast<P2PClientMeta>(meta);
|
||||
if (p2p_meta) {
|
||||
p2p_meta->SetSyncing(true);
|
||||
}
|
||||
}
|
||||
|
||||
HeartbeatTaskResult ProcessTask(const UUID& client_id,
|
||||
const HeartbeatTask& task) override;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_meta.h"
|
||||
#include "p2p_segment_manager.h"
|
||||
#include "p2p_rpc_types.h"
|
||||
#include "heartbeat_type.h"
|
||||
|
||||
namespace mooncake {
|
||||
class P2PClientMeta final : public ClientMeta {
|
||||
public:
|
||||
P2PClientMeta(const UUID& client_id, const std::string& ip_address,
|
||||
uint16_t rpc_port);
|
||||
|
||||
std::shared_ptr<SegmentManager> GetSegmentManager() override;
|
||||
auto QueryIp(const UUID& client_id)
|
||||
-> tl::expected<std::vector<std::string>, ErrorCode> override;
|
||||
|
||||
auto UpdateSegmentUsages(const std::vector<TierUsageInfo>& usages)
|
||||
-> SyncSegmentMetaResult;
|
||||
|
||||
size_t GetAvailableCapacity() const;
|
||||
|
||||
const std::string& get_ip_address() const { return ip_address_; }
|
||||
uint16_t get_rpc_port() const { return rpc_port_; }
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief A wrapper function of collecting write route candidates from this
|
||||
* client in ForEachClient
|
||||
* @param req The write route request containing filter config
|
||||
* @param candidates Output: collected candidates
|
||||
* @return On error, returns an unexpected ErrorCode.
|
||||
* Otherwise, returns bool value to indicate whether collected
|
||||
* candidates are enough. If return `true`, ForEachClient will stop
|
||||
*/
|
||||
auto CollectWriteRouteCandidates(const WriteRouteRequest& req,
|
||||
std::vector<WriteCandidate>& candidates)
|
||||
-> tl::expected<bool, ErrorCode>;
|
||||
|
||||
public:
|
||||
void DoOnDisconnected() override {}
|
||||
void DoOnRecovered() override {}
|
||||
|
||||
// HA sync tracking
|
||||
void SetSyncing(bool syncing) {
|
||||
is_syncing_.store(syncing, std::memory_order_release);
|
||||
}
|
||||
bool IsSyncing() const {
|
||||
return is_syncing_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t INF_PRIORITY = 10000;
|
||||
|
||||
std::string ip_address_;
|
||||
uint16_t rpc_port_ = 0;
|
||||
std::shared_ptr<P2PSegmentManager> segment_manager_;
|
||||
|
||||
mutable SpinRWLock capacity_mutex_;
|
||||
size_t client_capacity_ GUARDED_BY(capacity_mutex_) = 0;
|
||||
size_t client_usage_ GUARDED_BY(capacity_mutex_) = 0;
|
||||
|
||||
std::atomic<bool> is_syncing_{false};
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,357 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <coroutine>
|
||||
#include <async_simple/Try.h>
|
||||
#include <async_simple/coro/Lazy.h>
|
||||
|
||||
#include "async_metadata_notifier.h"
|
||||
#include "client_service.h"
|
||||
#include "data_manager.h"
|
||||
#include "client_rpc_service.h"
|
||||
#include "ha_recovery_manager.h"
|
||||
#include "peer_client.h"
|
||||
#include "p2p_master_client.h"
|
||||
#include "route_cache.h"
|
||||
#include "task_handle.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class P2PClientService final : public ClientService {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor for P2PClientService.
|
||||
* @param local_ip IP address of the local node.
|
||||
* @param te_port TE port of the local node.
|
||||
* @param metadata_connstring Connection string for metadata server.
|
||||
* @param metrics_port Port for metrics HTTP server.
|
||||
* @param enable_metrics_http Whether to enable metrics HTTP server.
|
||||
* @param labels Optional labels for client metrics.
|
||||
*/
|
||||
P2PClientService(const std::string& local_ip, uint16_t te_port,
|
||||
const std::string& metadata_connstring,
|
||||
uint16_t metrics_port = 9003,
|
||||
bool enable_metrics_http = true,
|
||||
const std::map<std::string, std::string>& labels = {});
|
||||
|
||||
virtual ~P2PClientService();
|
||||
|
||||
ErrorCode Init(const P2PClientConfig& config);
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* 1. Stops heartbeat, RPC server, and all background threads of submodules.
|
||||
* 2. Rejects all incoming requests.
|
||||
*/
|
||||
void Stop() override;
|
||||
|
||||
/**
|
||||
* @brief Release internal resources.
|
||||
*/
|
||||
void Destroy() override;
|
||||
|
||||
/**
|
||||
* @brief Single put data for a key.
|
||||
* @param key The object key.
|
||||
* @param slices Data slices.
|
||||
* @param config Replicate configuration.
|
||||
* @return An ErrorCode indicating the status.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Put(const ObjectKey& key,
|
||||
std::vector<Slice>& slices,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
/**
|
||||
* @brief Batch put data for multiple keys.
|
||||
* currently.
|
||||
* @param keys The list of object keys.
|
||||
* @param batched_slices The list of data slices for each key.
|
||||
* @param config Replicate configuration.
|
||||
* @return A vector of ErrorCode results for each key.
|
||||
*/
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPut(
|
||||
const std::vector<ObjectKey>& keys,
|
||||
std::vector<std::vector<Slice>>& batched_slices,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
/**
|
||||
* @brief Gets object metadata without transferring data
|
||||
* @param object_key Key to query
|
||||
* @return QueryResult containing replicas, or ErrorCode
|
||||
* indicating failure
|
||||
*/
|
||||
tl::expected<std::unique_ptr<QueryResult>, ErrorCode> Query(
|
||||
const std::string& object_key,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Batch query object metadata without transferring data
|
||||
* @param object_keys Keys to query
|
||||
* @return Vector of QueryResult objects containing replicas
|
||||
*/
|
||||
std::vector<tl::expected<std::unique_ptr<QueryResult>, ErrorCode>>
|
||||
BatchQuery(const std::vector<std::string>& object_keys,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
tl::expected<bool, ErrorCode> IsExist(const std::string& key) override;
|
||||
|
||||
std::vector<tl::expected<bool, ErrorCode>> BatchIsExist(
|
||||
const std::vector<std::string>& keys) override;
|
||||
|
||||
DeploymentMode deployment_mode() const override {
|
||||
return DeploymentMode::P2P;
|
||||
}
|
||||
|
||||
tl::expected<std::shared_ptr<BufferHandle>, ErrorCode> Get(
|
||||
const std::string& key,
|
||||
std::shared_ptr<ClientBufferAllocator> allocator,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::vector<tl::expected<std::shared_ptr<BufferHandle>, ErrorCode>>
|
||||
BatchGet(const std::vector<std::string>& keys,
|
||||
std::shared_ptr<ClientBufferAllocator> allocator,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
tl::expected<int64_t, ErrorCode> Get(
|
||||
const std::string& key, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
std::vector<tl::expected<int64_t, ErrorCode>> BatchGet(
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
const ReadRouteConfig& config = {},
|
||||
bool aggregate_same_segment_task = false) override;
|
||||
|
||||
/**
|
||||
* @brief Mount a memory segment in P2P mode.
|
||||
* @param buffer Start address of the buffer.
|
||||
* @param size Size of the buffer in bytes.
|
||||
* @return An ErrorCode indicating success or failure.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> MountSegment(const void* buffer,
|
||||
size_t size) override;
|
||||
|
||||
/**
|
||||
* @brief Unmount a memory segment in P2P mode.
|
||||
* @param buffer Start address of the buffer.
|
||||
* @param size Size of the buffer in bytes.
|
||||
* @return An ErrorCode indicating success or failure.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> UnmountSegment(const void* buffer,
|
||||
size_t size) override;
|
||||
|
||||
/**
|
||||
* @brief Removes an object and all its replicas
|
||||
* @param key Key to remove
|
||||
* @return ErrorCode indicating success/failure
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Remove(const ObjectKey& key) override;
|
||||
|
||||
/**
|
||||
* @brief Removes objects from the store whose keys match a regex pattern.
|
||||
* @param str The regular expression string to match against object keys.
|
||||
* @return An expected object containing the number of removed objects on
|
||||
* success, or an ErrorCode on failure.
|
||||
*/
|
||||
tl::expected<long, ErrorCode> RemoveByRegex(const ObjectKey& str) override;
|
||||
|
||||
/**
|
||||
* @brief Removes all objects and all its replicas
|
||||
* @return tl::expected<long, ErrorCode> number of removed objects or error
|
||||
*/
|
||||
tl::expected<long, ErrorCode> RemoveAll() override;
|
||||
|
||||
MasterClient& GetMasterClient() override { return master_client_; }
|
||||
|
||||
std::string GetHealthStatus() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief init TieredBackend and DataManager
|
||||
* 1. build metadata and segment sync callback
|
||||
* 2. build tiered config
|
||||
* 3. init tiered backend and data manager
|
||||
*/
|
||||
ErrorCode InitStorage(const P2PClientConfig& config);
|
||||
/**
|
||||
* @brief build add replica callback.
|
||||
* when tier add replica, call master to update metadata
|
||||
*/
|
||||
AddReplicaCallback BuildAddReplicaCallback();
|
||||
|
||||
/**
|
||||
* @brief build remove replica callback.
|
||||
* when tier remove replica, call master to update metadata
|
||||
*/
|
||||
RemoveReplicaCallback BuildRemoveReplicaCallback();
|
||||
|
||||
/**
|
||||
* @brief build segment sync callback.
|
||||
* when tier add/remove segment, call master to mount/unmount segment
|
||||
*/
|
||||
SegmentSyncCallback BuildSegmentSyncCallback();
|
||||
|
||||
/**
|
||||
* @brief handle COMMIT type callback: notify master to add new replica
|
||||
*/
|
||||
tl::expected<void, ErrorCode> SyncAddReplica(const std::string& key,
|
||||
const UUID& tier_id,
|
||||
size_t size);
|
||||
|
||||
/**
|
||||
* @brief handle DELETE type callback: notify master to remove replica
|
||||
*/
|
||||
tl::expected<void, ErrorCode> SyncRemoveReplica(const std::string& key,
|
||||
const UUID& tier_id);
|
||||
|
||||
/**
|
||||
* @brief handle batch DELETE: notify master to remove replicas from
|
||||
* multiple segments in one RPC call
|
||||
* @param key Key to remove
|
||||
* @param segment_ids Vector of segment IDs to remove (it will be moved)
|
||||
* @return Vector of ErrorCode results for each segment
|
||||
*/
|
||||
std::vector<tl::expected<void, ErrorCode>> SyncBatchRemoveReplica(
|
||||
const std::string& key, std::vector<UUID> segment_ids);
|
||||
|
||||
/**
|
||||
* @brief Collect tier info from DataManager and build P2P Segments.
|
||||
*/
|
||||
std::vector<Segment> CollectTierSegments() const;
|
||||
|
||||
/**
|
||||
* @brief Register the P2P client with the master server.
|
||||
* Collects segments from mounted_segments_ and registers them.
|
||||
* @return An ErrorCode indicating success or failure.
|
||||
*/
|
||||
tl::expected<RegisterClientResponse, ErrorCode> RegisterClient() override;
|
||||
|
||||
HeartbeatRequest build_heartbeat_request() override;
|
||||
|
||||
private:
|
||||
struct ResolvedRoute {
|
||||
PeerClient* peer = nullptr;
|
||||
uint64_t object_size = 0;
|
||||
bool is_cached = false;
|
||||
P2PProxyDescriptor proxy; // for RemoveReplica on stale-cache eviction
|
||||
};
|
||||
|
||||
// Yields ResolvedRoute candidates from cache first, then a one-shot lazy
|
||||
// master fallback. Call Prime() to pre-load before accessing object_size().
|
||||
class RouteIterator {
|
||||
public:
|
||||
using MasterFetch = std::function<
|
||||
async_simple::coro::Lazy<std::vector<ResolvedRoute>>()>;
|
||||
|
||||
RouteIterator(std::string key, std::vector<ResolvedRoute> initial,
|
||||
uint64_t object_size, RouteCache* route_cache,
|
||||
MasterFetch master_fetch);
|
||||
|
||||
uint64_t object_size() const { return object_size_; }
|
||||
bool empty() const { return routes_.empty() && master_queried_; }
|
||||
|
||||
void Prime();
|
||||
async_simple::coro::Lazy<std::optional<ResolvedRoute>> AsyncNext();
|
||||
void Evict(const ResolvedRoute& route);
|
||||
|
||||
private:
|
||||
void UpsertToCache(const std::vector<ResolvedRoute>& routes);
|
||||
|
||||
std::string key_;
|
||||
std::vector<ResolvedRoute> routes_;
|
||||
size_t idx_ = 0;
|
||||
bool master_queried_ = false;
|
||||
uint64_t object_size_ = 0;
|
||||
RouteCache* route_cache_ = nullptr;
|
||||
MasterFetch master_fetch_;
|
||||
};
|
||||
|
||||
tl::expected<RouteIterator, ErrorCode> BuildRouteIter(
|
||||
const std::string& key, const ReadRouteConfig& config);
|
||||
|
||||
async_simple::coro::Lazy<std::vector<ResolvedRoute>>
|
||||
AsyncResolveRoutesFromMaster(const std::string& key,
|
||||
const ReadRouteConfig& config);
|
||||
|
||||
static async_simple::coro::Lazy<void> RunReadRetry(
|
||||
RouteIterator iter, std::shared_ptr<RemoteReadRequest> req,
|
||||
std::shared_ptr<std::promise<tl::expected<void, ErrorCode>>> promise);
|
||||
|
||||
private:
|
||||
// Fetch write routes for all keys in one master RPC.
|
||||
tl::expected<BatchGetWriteRouteResponse, ErrorCode> BatchFetchWriteRoutes(
|
||||
const std::vector<ObjectKey>& keys,
|
||||
const std::vector<std::vector<Slice>>& batched_slices,
|
||||
const WriteRouteRequestConfig& config);
|
||||
|
||||
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode> CreatePutHandle(
|
||||
const std::string& key, std::vector<Slice>& slices,
|
||||
const WriteRouteRequestConfig& config);
|
||||
|
||||
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode>
|
||||
CreateLocalPutHandle(const std::string& key, std::vector<Slice>& slices);
|
||||
|
||||
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode>
|
||||
InnerCreatePutHandle(const std::string& key, std::vector<Slice>& slices,
|
||||
const WriteRouteRequestConfig& config,
|
||||
std::vector<WriteCandidate> candidates);
|
||||
|
||||
tl::expected<ReadTaskHandle, ErrorCode> CreateGetHandle(
|
||||
const std::string& key,
|
||||
std::shared_ptr<ClientBufferAllocator> allocator,
|
||||
const ReadRouteConfig& config);
|
||||
|
||||
tl::expected<ReadTaskHandle, ErrorCode> CreateGetHandle(
|
||||
const std::string& key, std::vector<Slice>& slices,
|
||||
const ReadRouteConfig& config);
|
||||
|
||||
/**
|
||||
* @brief Launch async reads driven by a RouteIterator.
|
||||
*
|
||||
* Creates a ReadRetryContinuation that fires the first RPC immediately
|
||||
* and chains subsequent candidates on failure (no stack recursion).
|
||||
*/
|
||||
tl::expected<ReadTaskHandle, ErrorCode> InnerGetViaRoute(
|
||||
const std::string& key, std::vector<Slice>& slices, RouteIterator iter);
|
||||
|
||||
/**
|
||||
* @brief Get or create a PeerClient for the given endpoint.
|
||||
* Thread-safe via peer_clients_mutex_.
|
||||
*/
|
||||
PeerClient& GetOrCreatePeerClient(const std::string& endpoint);
|
||||
|
||||
private:
|
||||
void OnHAEvent(HAEvent event) override;
|
||||
|
||||
private:
|
||||
P2PMasterClient master_client_;
|
||||
uint16_t client_rpc_port_ = 12345;
|
||||
|
||||
std::unique_ptr<coro_rpc::coro_rpc_server> client_rpc_server_;
|
||||
std::thread client_rpc_server_thread_;
|
||||
std::optional<DataManager> data_manager_;
|
||||
std::optional<ClientRpcService> client_rpc_service_;
|
||||
|
||||
// Each PeerClient instance maintains its own fixed-size connection pool.
|
||||
std::mutex peer_clients_mutex_;
|
||||
std::map<std::string, std::unique_ptr<PeerClient>> peer_clients_;
|
||||
|
||||
// Route cache for reducing Master query pressure
|
||||
std::optional<RouteCache> route_cache_;
|
||||
|
||||
// Async route notifier (nullptr when disabled)
|
||||
std::unique_ptr<AsyncMetadataNotifier> async_route_notifier_;
|
||||
|
||||
// HA recovery manager
|
||||
std::unique_ptr<HARecoveryManager> ha_manager_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
#pragma once
|
||||
|
||||
#include "master_client.h"
|
||||
#include "p2p_rpc_types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @brief Client for interacting with the mooncake P2P master service
|
||||
*/
|
||||
class P2PMasterClient final : public MasterClient {
|
||||
public:
|
||||
P2PMasterClient(const UUID& client_id,
|
||||
MasterClientMetric* metrics = nullptr)
|
||||
: MasterClient(client_id, metrics) {}
|
||||
|
||||
P2PMasterClient(const P2PMasterClient&) = delete;
|
||||
P2PMasterClient& operator=(const P2PMasterClient&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Gets write candidate route for a segment
|
||||
*/
|
||||
[[nodiscard]] tl::expected<WriteRouteResponse, ErrorCode> GetWriteRoute(
|
||||
const WriteRouteRequest& req);
|
||||
|
||||
/**
|
||||
* @brief Batch gets write candidate routes for multiple keys in one RPC
|
||||
*/
|
||||
[[nodiscard]] tl::expected<BatchGetWriteRouteResponse, ErrorCode>
|
||||
BatchGetWriteRoute(const BatchGetWriteRouteRequest& req);
|
||||
|
||||
/**
|
||||
* @brief Adds a replica to master
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> AddReplica(
|
||||
const AddReplicaRequest& req);
|
||||
|
||||
/**
|
||||
* @brief Removes a replica from master
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> RemoveReplica(
|
||||
const RemoveReplicaRequest& req);
|
||||
|
||||
/**
|
||||
* @brief Removes replicas from multiple segments in one call
|
||||
*/
|
||||
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchRemoveReplica(
|
||||
const BatchRemoveReplicaRequest& req);
|
||||
|
||||
/**
|
||||
* @brief Batch sync replicas with mixed ADD and REMOVE ops
|
||||
*/
|
||||
[[nodiscard]] tl::expected<BatchSyncReplicaResponse, ErrorCode>
|
||||
BatchSyncReplica(const BatchSyncReplicaRequest& req);
|
||||
|
||||
/**
|
||||
* @brief Notify Master that this client has finished syncing metadata
|
||||
*/
|
||||
[[nodiscard]] tl::expected<void, ErrorCode> SetSyncCompleted(
|
||||
UUID client_id);
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
#pragma once
|
||||
|
||||
#include "master_service.h"
|
||||
#include "p2p_client_manager.h"
|
||||
#include "p2p_rpc_types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class P2PMasterService : public MasterService {
|
||||
public:
|
||||
explicit P2PMasterService(const MasterServiceConfig& config);
|
||||
~P2PMasterService() override = default;
|
||||
|
||||
ClientManager& GetClientManager() override { return *client_manager_; }
|
||||
const ClientManager& GetClientManager() const override {
|
||||
return *client_manager_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get write route based on the config in the request
|
||||
*/
|
||||
auto GetWriteRoute(const WriteRouteRequest& req)
|
||||
-> tl::expected<WriteRouteResponse, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Batch get write routes for multiple keys.
|
||||
* Reuses GetWriteRoute logic per key.
|
||||
*/
|
||||
auto BatchGetWriteRoute(const BatchGetWriteRouteRequest& req)
|
||||
-> BatchGetWriteRouteResponse;
|
||||
|
||||
/**
|
||||
* @brief Add a route replica to master
|
||||
*/
|
||||
auto AddReplica(const AddReplicaRequest& req)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Remove a route replica from master
|
||||
*/
|
||||
auto RemoveReplica(const RemoveReplicaRequest& req)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
|
||||
/**
|
||||
* @brief Remove replicas from multiple segments in one call
|
||||
*/
|
||||
auto BatchRemoveReplica(const BatchRemoveReplicaRequest& req)
|
||||
-> std::vector<tl::expected<void, ErrorCode>>;
|
||||
|
||||
/**
|
||||
* @brief Batch sync replicas with mixed ADD and REMOVE ops
|
||||
*/
|
||||
auto BatchSyncReplica(const BatchSyncReplicaRequest& req)
|
||||
-> BatchSyncReplicaResponse;
|
||||
|
||||
/**
|
||||
* @brief Client notifies Master that metadata sync is complete
|
||||
*/
|
||||
auto SetSyncCompleted(UUID client_id) -> tl::expected<void, ErrorCode>;
|
||||
|
||||
std::vector<Replica::Descriptor> FilterReplicas(
|
||||
const GetReplicaListRequestConfig& config,
|
||||
const ObjectMetadata& metadata) override;
|
||||
|
||||
protected:
|
||||
typedef MetadataShard P2PMetadataShard;
|
||||
MetadataShard& GetShard(size_t idx) override {
|
||||
return metadata_shards_[idx];
|
||||
}
|
||||
const MetadataShard& GetShard(size_t idx) const override {
|
||||
return metadata_shards_[idx];
|
||||
}
|
||||
|
||||
P2PMetadataShard& GetP2PShard(size_t idx) { return metadata_shards_[idx]; }
|
||||
const P2PMetadataShard& GetP2PShard(size_t idx) const {
|
||||
return metadata_shards_[idx];
|
||||
}
|
||||
static constexpr size_t kNumShards = 1024; // Number of metadata shards
|
||||
// Helper to get shard index from key
|
||||
size_t GetShardIndex(const std::string& key) const override {
|
||||
return std::hash<std::string>{}(key) % kNumShards;
|
||||
}
|
||||
size_t GetShardCount() const override { return kNumShards; }
|
||||
|
||||
protected:
|
||||
// Hooks
|
||||
void OnObjectAccessed(ObjectMetadata& metadata) override;
|
||||
void OnObjectHit(const ObjectMetadata& metadata) override;
|
||||
void OnReplicaRemoved(const Replica& replica) override;
|
||||
void OnReplicaAdded(const Replica& replica) override;
|
||||
|
||||
private:
|
||||
tl::expected<void, ErrorCode> InnerAddReplica(
|
||||
MetadataShard& shard, const std::string& key, const UUID& client_id,
|
||||
const UUID& segment_id, size_t size,
|
||||
const std::shared_ptr<P2PClientMeta>& client) NO_THREAD_SAFETY_ANALYSIS;
|
||||
tl::expected<void, ErrorCode> InnerRemoveReplica(
|
||||
MetadataShard& shard, const std::string& key, const UUID& client_id,
|
||||
const UUID& segment_id) NO_THREAD_SAFETY_ANALYSIS;
|
||||
|
||||
std::shared_ptr<P2PClientManager> client_manager_;
|
||||
std::array<P2PMetadataShard, kNumShards> metadata_shards_;
|
||||
// for the number of replicas of a key:
|
||||
// 1. max_replicas_per_key_ == 0 means no limitation
|
||||
// 2. max_replicas_per_key_ > 0 means the max replica number of a key
|
||||
uint64_t max_replicas_per_key_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
|
||||
#include "p2p_master_service.h"
|
||||
#include "rpc_service.h"
|
||||
|
||||
#include "p2p_rpc_types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class WrappedP2PMasterService final : public WrappedMasterService {
|
||||
public:
|
||||
WrappedP2PMasterService(const WrappedMasterServiceConfig& config);
|
||||
|
||||
~WrappedP2PMasterService() override = default;
|
||||
|
||||
MasterService& GetMasterService() override { return master_service_; }
|
||||
|
||||
tl::expected<WriteRouteResponse, ErrorCode> GetWriteRoute(
|
||||
const WriteRouteRequest& req);
|
||||
|
||||
BatchGetWriteRouteResponse BatchGetWriteRoute(
|
||||
const BatchGetWriteRouteRequest& req);
|
||||
|
||||
tl::expected<void, ErrorCode> AddReplica(const AddReplicaRequest& req);
|
||||
|
||||
tl::expected<void, ErrorCode> RemoveReplica(
|
||||
const RemoveReplicaRequest& req);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchRemoveReplica(
|
||||
const BatchRemoveReplicaRequest& req);
|
||||
|
||||
BatchSyncReplicaResponse BatchSyncReplica(
|
||||
const BatchSyncReplicaRequest& req);
|
||||
|
||||
tl::expected<void, ErrorCode> SetSyncCompleted(UUID client_id);
|
||||
|
||||
private:
|
||||
P2PMasterService master_service_;
|
||||
};
|
||||
|
||||
void RegisterP2PRpcService(
|
||||
coro_rpc::coro_rpc_server& server,
|
||||
mooncake::WrappedP2PMasterService& wrapped_master_service);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "replica.h"
|
||||
#include "types.h"
|
||||
#include <ylt/reflection/user_reflect_macro.hpp>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @brief Request config for write route
|
||||
*/
|
||||
struct WriteRouteRequestConfig {
|
||||
static constexpr size_t RETURN_ALL_CANDIDATES = 0;
|
||||
size_t max_candidates = RETURN_ALL_CANDIDATES;
|
||||
ObjectIterateStrategy strategy = ObjectIterateStrategy::CAPACITY_PRIORITY;
|
||||
bool allow_local = true; // whether to filter local client
|
||||
bool prefer_local = true; // enhance the priority of local client
|
||||
// works only when allow_local==true
|
||||
bool early_return = true; // whether to return immediately once candidates
|
||||
// meet conditions of config
|
||||
|
||||
// segment level (TODO)
|
||||
// filter the segment with tag
|
||||
std::vector<std::string> tag_filters;
|
||||
// filter the segments whose priority is lower than priority_limit
|
||||
int priority_limit = 0;
|
||||
};
|
||||
YLT_REFL(WriteRouteRequestConfig, max_candidates, strategy, allow_local,
|
||||
prefer_local, early_return, tag_filters, priority_limit);
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os,
|
||||
const WriteRouteRequestConfig& config) {
|
||||
os << "WriteRouteRequestConfig: { max_candidates: " << config.max_candidates
|
||||
<< ", strategy: " << config.strategy
|
||||
<< ", allow_local: " << (config.allow_local ? "true" : "false")
|
||||
<< ", prefer_local: " << (config.prefer_local ? "true" : "false")
|
||||
<< ", early_return: " << (config.early_return ? "true" : "false")
|
||||
<< ", priority_limit: " << config.priority_limit << " }";
|
||||
return os;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Request structure for getting write route.
|
||||
*/
|
||||
struct WriteRouteRequest {
|
||||
std::string key; // used for pre-filter with limitation of replica number
|
||||
UUID client_id;
|
||||
size_t size = 0;
|
||||
WriteRouteRequestConfig config;
|
||||
};
|
||||
YLT_REFL(WriteRouteRequest, key, client_id, size, config);
|
||||
|
||||
/**
|
||||
* @brief Candidate node for writing route
|
||||
*/
|
||||
struct WriteCandidate {
|
||||
P2PProxyDescriptor replica;
|
||||
size_t available_capacity = 0;
|
||||
int priority = 0;
|
||||
};
|
||||
YLT_REFL(WriteCandidate, replica, available_capacity, priority);
|
||||
|
||||
/**
|
||||
* @brief Response structure for getting write route.
|
||||
*/
|
||||
struct WriteRouteResponse {
|
||||
std::vector<WriteCandidate> candidates;
|
||||
};
|
||||
YLT_REFL(WriteRouteResponse, candidates);
|
||||
|
||||
/**
|
||||
* @brief Request for batch write route lookup.
|
||||
*/
|
||||
struct BatchGetWriteRouteRequest {
|
||||
UUID client_id;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<size_t> sizes;
|
||||
WriteRouteRequestConfig config; // shared config for all keys
|
||||
};
|
||||
YLT_REFL(BatchGetWriteRouteRequest, client_id, keys, sizes, config);
|
||||
|
||||
/**
|
||||
* @brief Response for batch write route lookup.
|
||||
* responses[i] and error_codes[i] correspond to keys[i] in the request.
|
||||
*/
|
||||
struct BatchGetWriteRouteResponse {
|
||||
std::vector<WriteRouteResponse> responses; // valid when error_codes[i]==OK
|
||||
std::vector<ErrorCode> error_codes;
|
||||
};
|
||||
YLT_REFL(BatchGetWriteRouteResponse, responses, error_codes);
|
||||
|
||||
/**
|
||||
* @brief Request to add a replica.
|
||||
* Master resolves ip_address/rpc_port from registered client info.
|
||||
*/
|
||||
struct AddReplicaRequest {
|
||||
std::string key;
|
||||
size_t size;
|
||||
UUID client_id;
|
||||
UUID segment_id;
|
||||
};
|
||||
YLT_REFL(AddReplicaRequest, key, size, client_id, segment_id);
|
||||
|
||||
/**
|
||||
* @brief Request to remove a replica
|
||||
*/
|
||||
struct RemoveReplicaRequest {
|
||||
std::string key;
|
||||
UUID client_id;
|
||||
UUID segment_id;
|
||||
};
|
||||
YLT_REFL(RemoveReplicaRequest, key, client_id, segment_id);
|
||||
|
||||
/**
|
||||
* @brief Request to remove replicas from multiple segments in one call
|
||||
*/
|
||||
struct BatchRemoveReplicaRequest {
|
||||
std::string key;
|
||||
UUID client_id;
|
||||
std::vector<UUID> segment_ids;
|
||||
};
|
||||
YLT_REFL(BatchRemoveReplicaRequest, key, client_id, segment_ids);
|
||||
|
||||
/**
|
||||
* @brief Request to batch sync replicas (mixed ADD and REMOVE ops).
|
||||
* Master only needs client_id + segment_id to identify replicas
|
||||
*/
|
||||
struct BatchSyncReplicaRequest {
|
||||
UUID client_id;
|
||||
// ADD operations
|
||||
std::vector<std::string> add_keys;
|
||||
std::vector<size_t> add_sizes;
|
||||
std::vector<UUID> add_segment_ids;
|
||||
// REMOVE operations
|
||||
std::vector<std::string> remove_keys;
|
||||
std::vector<UUID> remove_segment_ids;
|
||||
};
|
||||
YLT_REFL(BatchSyncReplicaRequest, client_id, add_keys, add_sizes,
|
||||
add_segment_ids, remove_keys, remove_segment_ids);
|
||||
|
||||
/**
|
||||
* @brief Response for batch sync replicas.
|
||||
*/
|
||||
struct BatchSyncReplicaResponse {
|
||||
std::vector<ErrorCode> add_results;
|
||||
std::vector<ErrorCode> remove_results;
|
||||
};
|
||||
YLT_REFL(BatchSyncReplicaResponse, add_results, remove_results);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include "segment_manager.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
class P2PSegmentManager : public SegmentManager {
|
||||
public:
|
||||
auto QuerySegments(const std::string& segment)
|
||||
-> tl::expected<std::pair<size_t, size_t>, ErrorCode> override;
|
||||
|
||||
using OnSegmentAddedCallback = std::function<void(const Segment& segment)>;
|
||||
using OnSegmentRemovedCallback =
|
||||
std::function<void(const Segment& segment)>;
|
||||
|
||||
void SetSegmentChangeCallbacks(OnSegmentAddedCallback on_add,
|
||||
OnSegmentRemovedCallback on_remove) {
|
||||
on_segment_added_ = std::move(on_add);
|
||||
on_segment_removed_ = std::move(on_remove);
|
||||
}
|
||||
/**
|
||||
* @brief update segment usage and return old usage
|
||||
*/
|
||||
tl::expected<size_t, ErrorCode> UpdateSegmentUsage(const UUID& segment_id,
|
||||
size_t usage);
|
||||
|
||||
/**
|
||||
* @brief get segment usage
|
||||
*/
|
||||
size_t GetSegmentUsage(const UUID& segment_id) const;
|
||||
|
||||
/**
|
||||
* @brief Iterate over all mounted P2P segments under a single read lock.
|
||||
* Visitor returns true to stop early.
|
||||
*/
|
||||
using SegmentVisitor = std::function<bool(const Segment& segment)>;
|
||||
void ForEachSegment(const SegmentVisitor& visitor) const;
|
||||
|
||||
protected:
|
||||
tl::expected<void, ErrorCode> InnerMountSegment(
|
||||
const Segment& segment) override;
|
||||
|
||||
tl::expected<void, ErrorCode> OnUnmountSegment(
|
||||
const std::shared_ptr<Segment>& segment) override;
|
||||
|
||||
private:
|
||||
OnSegmentAddedCallback on_segment_added_;
|
||||
OnSegmentRemovedCallback on_segment_removed_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_rpc_types.h"
|
||||
#include "types.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <ylt/coro_rpc/coro_rpc_client.hpp>
|
||||
#include <ylt/coro_io/client_pool.hpp>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
#include <async_simple/coro/Lazy.h>
|
||||
#include <async_simple/coro/SyncAwait.h>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class PeerClient {
|
||||
public:
|
||||
tl::expected<void, ErrorCode> Connect(const std::string& endpoint);
|
||||
|
||||
// --- Async single-key interfaces ---
|
||||
async_simple::coro::Lazy<tl::expected<void, ErrorCode>> AsyncReadRemoteData(
|
||||
const RemoteReadRequest& request);
|
||||
|
||||
async_simple::coro::Lazy<tl::expected<UUID, ErrorCode>>
|
||||
AsyncWriteRemoteData(const RemoteWriteRequest& request);
|
||||
|
||||
// --- Sync single-key interfaces ---
|
||||
tl::expected<void, ErrorCode> ReadRemoteData(
|
||||
const RemoteReadRequest& request);
|
||||
tl::expected<UUID, ErrorCode> WriteRemoteData(
|
||||
const RemoteWriteRequest& request);
|
||||
|
||||
private:
|
||||
std::shared_ptr<coro_io::client_pools<coro_rpc::coro_rpc_client>>
|
||||
client_pools_;
|
||||
std::shared_ptr<coro_io::client_pool<coro_rpc::coro_rpc_client>>
|
||||
client_pool_;
|
||||
std::string endpoint_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -1,17 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include <csignal>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "client_service.h"
|
||||
#include "client_buffer.hpp"
|
||||
#include "mutex.h"
|
||||
#include "utils.h"
|
||||
#include "file_storage.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
|
|
@ -29,106 +23,96 @@ struct ShmRegisterRequest {
|
|||
class PyClient {
|
||||
public:
|
||||
virtual ~PyClient() = 0;
|
||||
virtual int setup_real(
|
||||
const std::string &local_hostname, const std::string &metadata_server,
|
||||
size_t global_segment_size, size_t local_buffer_size,
|
||||
const std::string &protocol, const std::string &rdma_devices,
|
||||
const std::string &master_server_addr,
|
||||
const std::shared_ptr<TransferEngine> &transfer_engine,
|
||||
const std::string &ipc_socket_path) = 0;
|
||||
|
||||
virtual int setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
|
||||
const std::string &server_address,
|
||||
const std::string &ipc_socket_path) = 0;
|
||||
|
||||
virtual int initAll(const std::string &protocol,
|
||||
const std::string &device_name,
|
||||
virtual int initAll(const std::string& protocol,
|
||||
const std::string& device_name,
|
||||
size_t mount_segment_size) = 0;
|
||||
|
||||
virtual uint64_t alloc_from_mem_pool(size_t size) = 0;
|
||||
|
||||
virtual int put(const std::string &key, std::span<const char> value,
|
||||
const ReplicateConfig &config = ReplicateConfig{}) = 0;
|
||||
virtual DeploymentMode deployment_mode() const = 0;
|
||||
|
||||
virtual int register_buffer(void *buffer, size_t size) = 0;
|
||||
virtual int put(const std::string& key, std::span<const char> value,
|
||||
const WriteConfig& config) = 0;
|
||||
|
||||
virtual int unregister_buffer(void *buffer) = 0;
|
||||
virtual int register_buffer(void* buffer, size_t size) = 0;
|
||||
|
||||
virtual int64_t get_into(const std::string &key, void *buffer,
|
||||
size_t size) = 0;
|
||||
virtual int unregister_buffer(void* buffer) = 0;
|
||||
|
||||
virtual int64_t get_into(const std::string& key, void* buffer, size_t size,
|
||||
const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
virtual std::vector<int64_t> batch_get_into(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<void *> &buffers,
|
||||
const std::vector<size_t> &sizes) = 0;
|
||||
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
virtual std::vector<int> batch_get_into_multi_buffers(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::vector<void *>> &all_buffers,
|
||||
const std::vector<std::vector<size_t>> &all_sizes,
|
||||
bool prefer_same_node) = 0;
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
bool aggregate_same_segment_task,
|
||||
const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
virtual int put_from(const std::string &key, void *buffer, size_t size,
|
||||
const ReplicateConfig &config = ReplicateConfig{}) = 0;
|
||||
virtual int put_from(const std::string& key, void* buffer, size_t size,
|
||||
const WriteConfig& config) = 0;
|
||||
|
||||
virtual int put_from_with_metadata(
|
||||
const std::string &key, void *buffer, void *metadata_buffer,
|
||||
size_t size, size_t metadata_size,
|
||||
const ReplicateConfig &config = ReplicateConfig{}) = 0;
|
||||
virtual int put_from_with_metadata(const std::string& key, void* buffer,
|
||||
void* metadata_buffer, size_t size,
|
||||
size_t metadata_size,
|
||||
const WriteConfig& config) = 0;
|
||||
|
||||
virtual std::vector<int> batch_put_from(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<void *> &buffers, const std::vector<size_t> &sizes,
|
||||
const ReplicateConfig &config = ReplicateConfig{}) = 0;
|
||||
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes, const WriteConfig& config) = 0;
|
||||
|
||||
virtual std::vector<int> batch_put_from_multi_buffers(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::vector<void *>> &all_buffers,
|
||||
const std::vector<std::vector<size_t>> &all_sizes,
|
||||
const ReplicateConfig &config = ReplicateConfig{}) = 0;
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
const WriteConfig& config) = 0;
|
||||
|
||||
virtual std::shared_ptr<BufferHandle> get_buffer(
|
||||
const std::string &key) = 0;
|
||||
const std::string& key, const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
virtual std::tuple<uint64_t, size_t> get_buffer_info(
|
||||
const std::string &key) = 0;
|
||||
const std::string& key, const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
virtual std::vector<std::shared_ptr<BufferHandle>> batch_get_buffer(
|
||||
const std::vector<std::string> &keys) = 0;
|
||||
const std::vector<std::string>& keys,
|
||||
const ReadRouteConfig& config = {}) = 0;
|
||||
|
||||
virtual int put_parts(
|
||||
const std::string &key, std::vector<std::span<const char>> values,
|
||||
const ReplicateConfig &config = ReplicateConfig{}) = 0;
|
||||
virtual int put_parts(const std::string& key,
|
||||
std::vector<std::span<const char>> values,
|
||||
const WriteConfig& config) = 0;
|
||||
|
||||
virtual int put_batch(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::span<const char>> &values,
|
||||
const ReplicateConfig &config = ReplicateConfig{}) = 0;
|
||||
virtual int put_batch(const std::vector<std::string>& keys,
|
||||
const std::vector<std::span<const char>>& values,
|
||||
const WriteConfig& config) = 0;
|
||||
|
||||
[[nodiscard]] virtual std::string get_hostname() const = 0;
|
||||
|
||||
virtual int remove(const std::string &key) = 0;
|
||||
virtual int remove(const std::string& key) = 0;
|
||||
|
||||
virtual long removeByRegex(const std::string &str) = 0;
|
||||
virtual long removeByRegex(const std::string& str) = 0;
|
||||
|
||||
virtual long removeAll() = 0;
|
||||
|
||||
virtual int isExist(const std::string &key) = 0;
|
||||
virtual int isExist(const std::string& key) = 0;
|
||||
|
||||
virtual std::vector<int> batchIsExist(
|
||||
const std::vector<std::string> &keys) = 0;
|
||||
const std::vector<std::string>& keys) = 0;
|
||||
|
||||
virtual int64_t getSize(const std::string &key) = 0;
|
||||
virtual int64_t getSize(const std::string& key) = 0;
|
||||
|
||||
virtual std::map<std::string, std::vector<Replica::Descriptor>>
|
||||
batch_get_replica_desc(const std::vector<std::string> &keys) = 0;
|
||||
batch_get_replica_desc(const std::vector<std::string>& keys) = 0;
|
||||
virtual std::vector<Replica::Descriptor> get_replica_desc(
|
||||
const std::string &key) = 0;
|
||||
const std::string& key) = 0;
|
||||
|
||||
virtual int tearDownAll() = 0;
|
||||
|
||||
std::shared_ptr<mooncake::Client> client_ = nullptr;
|
||||
std::shared_ptr<mooncake::FileStorage> file_storage_ = nullptr;
|
||||
std::shared_ptr<mooncake::ClientService> client_service_ = nullptr;
|
||||
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator_ = nullptr;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include "client_config_builder.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <boost/lockfree/queue.hpp>
|
||||
#include <csignal>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
#include "pyclient.h"
|
||||
#include "client_service.h"
|
||||
#include "client_buffer.hpp"
|
||||
#include "mutex.h"
|
||||
#include "utils.h"
|
||||
|
|
@ -24,18 +25,18 @@ class RealClient;
|
|||
class ResourceTracker {
|
||||
public:
|
||||
// Get the singleton instance
|
||||
static ResourceTracker &getInstance();
|
||||
static ResourceTracker& getInstance();
|
||||
|
||||
// Register a DistributedObjectStore instance for cleanup
|
||||
void registerInstance(const std::shared_ptr<PyClient> &instance);
|
||||
void registerInstance(const std::shared_ptr<PyClient>& instance);
|
||||
|
||||
private:
|
||||
ResourceTracker();
|
||||
~ResourceTracker();
|
||||
|
||||
// Prevent copying
|
||||
ResourceTracker(const ResourceTracker &) = delete;
|
||||
ResourceTracker &operator=(const ResourceTracker &) = delete;
|
||||
ResourceTracker(const ResourceTracker&) = delete;
|
||||
ResourceTracker& operator=(const ResourceTracker&) = delete;
|
||||
|
||||
// Cleanup all registered resources
|
||||
void cleanupAllResources();
|
||||
|
|
@ -66,34 +67,25 @@ class RealClient : public PyClient {
|
|||
// Factory to create shared instances and auto-register to ResourceTracker
|
||||
static std::shared_ptr<RealClient> create();
|
||||
|
||||
int setup_real(
|
||||
const std::string &local_hostname, const std::string &metadata_server,
|
||||
size_t global_segment_size = 1024 * 1024 * 16,
|
||||
size_t local_buffer_size = 1024 * 1024 * 16,
|
||||
const std::string &protocol = "tcp",
|
||||
const std::string &rdma_devices = "",
|
||||
const std::string &master_server_addr = "127.0.0.1:50051",
|
||||
const std::shared_ptr<TransferEngine> &transfer_engine = nullptr,
|
||||
const std::string &ipc_socket_path = "");
|
||||
template <typename ConfigT>
|
||||
int setup(ConfigT& config);
|
||||
|
||||
int setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
|
||||
const std::string &server_address,
|
||||
const std::string &ipc_socket_path) {
|
||||
// Real client does not support dummy setup
|
||||
return -1;
|
||||
};
|
||||
int initAll(const std::string& protocol, const std::string& device_name,
|
||||
size_t mount_segment_size = 1024 * 1024 * 16) override;
|
||||
|
||||
int initAll(const std::string &protocol, const std::string &device_name,
|
||||
size_t mount_segment_size = 1024 * 1024 * 16); // Default 16MB
|
||||
uint64_t alloc_from_mem_pool(size_t size) override { return 0; };
|
||||
|
||||
uint64_t alloc_from_mem_pool(size_t size) { return 0; };
|
||||
DeploymentMode deployment_mode() const override {
|
||||
return client_service_ ? client_service_->deployment_mode()
|
||||
: DeploymentMode::UNKNOWN;
|
||||
}
|
||||
|
||||
int put(const std::string &key, std::span<const char> value,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
int put(const std::string& key, std::span<const char> value,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
int register_buffer(void *buffer, size_t size);
|
||||
int register_buffer(void* buffer, size_t size) override;
|
||||
|
||||
int unregister_buffer(void *buffer);
|
||||
int unregister_buffer(void* buffer) override;
|
||||
|
||||
/**
|
||||
* @brief Get object data directly into a pre-allocated buffer
|
||||
|
|
@ -105,7 +97,8 @@ class RealClient : public PyClient {
|
|||
* @note The buffer address must be previously registered with
|
||||
* register_buffer() for zero-copy operations
|
||||
*/
|
||||
int64_t get_into(const std::string &key, void *buffer, size_t size);
|
||||
int64_t get_into(const std::string& key, void* buffer, size_t size,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Get object data directly into pre-allocated buffers for multiple
|
||||
|
|
@ -118,9 +111,10 @@ class RealClient : public PyClient {
|
|||
* @note The buffer addresses must be previously registered with
|
||||
* register_buffer() for zero-copy operations
|
||||
*/
|
||||
std::vector<int64_t> batch_get_into(const std::vector<std::string> &keys,
|
||||
const std::vector<void *> &buffers,
|
||||
const std::vector<size_t> &sizes);
|
||||
std::vector<int64_t> batch_get_into(
|
||||
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Get object data directly into pre-allocated buffers for multiple
|
||||
|
|
@ -135,10 +129,11 @@ class RealClient : public PyClient {
|
|||
* register_buffer() for zero-copy operations
|
||||
*/
|
||||
std::vector<int> batch_get_into_multi_buffers(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::vector<void *>> &all_buffers,
|
||||
const std::vector<std::vector<size_t>> &all_sizes,
|
||||
bool prefer_same_node);
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
bool aggregate_same_segment_task,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Put object data directly from a pre-allocated buffer
|
||||
|
|
@ -150,8 +145,8 @@ class RealClient : public PyClient {
|
|||
* @note The buffer address must be previously registered with
|
||||
* register_buffer() for zero-copy operations
|
||||
*/
|
||||
int put_from(const std::string &key, void *buffer, size_t size,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
int put_from(const std::string& key, void* buffer, size_t size,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
/**
|
||||
* @brief Put object data directly from pre-allocated buffers for multiple
|
||||
|
|
@ -168,10 +163,10 @@ class RealClient : public PyClient {
|
|||
* @note The buffer addresses must be previously registered with
|
||||
* register_buffer() for zero-copy operations
|
||||
*/
|
||||
int put_from_with_metadata(
|
||||
const std::string &key, void *buffer, void *metadata_buffer,
|
||||
size_t size, size_t metadata_size,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
int put_from_with_metadata(const std::string& key, void* buffer,
|
||||
void* metadata_buffer, size_t size,
|
||||
size_t metadata_size,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
/**
|
||||
* @brief Put object data directly from pre-allocated buffers for multiple
|
||||
|
|
@ -186,10 +181,10 @@ class RealClient : public PyClient {
|
|||
* register_buffer() for zero-copy operations
|
||||
*/
|
||||
|
||||
std::vector<int> batch_put_from(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<void *> &buffers, const std::vector<size_t> &sizes,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
std::vector<int> batch_put_from(const std::vector<std::string>& keys,
|
||||
const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
/**
|
||||
* @brief Put object data directly from multiple pre-allocated buffers for
|
||||
|
|
@ -205,20 +200,20 @@ class RealClient : public PyClient {
|
|||
* register_buffer() for zero-copy operations
|
||||
*/
|
||||
std::vector<int> batch_put_from_multi_buffers(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::vector<void *>> &all_buffers,
|
||||
const std::vector<std::vector<size_t>> &all_sizes,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
int put_parts(const std::string &key,
|
||||
int put_parts(const std::string& key,
|
||||
std::vector<std::span<const char>> values,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
const WriteConfig& config) override;
|
||||
|
||||
int put_batch(const std::vector<std::string> &keys,
|
||||
const std::vector<std::span<const char>> &values,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
int put_batch(const std::vector<std::string>& keys,
|
||||
const std::vector<std::span<const char>>& values,
|
||||
const WriteConfig& config) override;
|
||||
|
||||
[[nodiscard]] std::string get_hostname() const;
|
||||
[[nodiscard]] std::string get_hostname() const override;
|
||||
|
||||
/**
|
||||
* @brief Get a buffer containing the data for a key
|
||||
|
|
@ -226,14 +221,16 @@ class RealClient : public PyClient {
|
|||
* @return std::shared_ptr<BufferHandle> Buffer containing the data, or
|
||||
* nullptr if error
|
||||
*/
|
||||
std::shared_ptr<BufferHandle> get_buffer(const std::string &key);
|
||||
std::shared_ptr<BufferHandle> get_buffer(
|
||||
const std::string& key, const ReadRouteConfig& config = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Get buffer information (address and size) for a key
|
||||
* @param key Key to get buffer information for
|
||||
* @return Tuple containing buffer address and size, or (0, 0) if error
|
||||
*/
|
||||
std::tuple<uint64_t, size_t> get_buffer_info(const std::string &key);
|
||||
std::tuple<uint64_t, size_t> get_buffer_info(
|
||||
const std::string& key, const ReadRouteConfig& config = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Get buffers containing the data for multiple keys (batch version)
|
||||
|
|
@ -242,22 +239,23 @@ class RealClient : public PyClient {
|
|||
* data, or nullptr for each key if error
|
||||
*/
|
||||
std::vector<std::shared_ptr<BufferHandle>> batch_get_buffer(
|
||||
const std::vector<std::string> &keys);
|
||||
const std::vector<std::string>& keys,
|
||||
const ReadRouteConfig& config = {}) override;
|
||||
|
||||
int remove(const std::string &key);
|
||||
int remove(const std::string& key) override;
|
||||
|
||||
long removeByRegex(const std::string &str);
|
||||
long removeByRegex(const std::string& str) override;
|
||||
|
||||
long removeAll();
|
||||
long removeAll() override;
|
||||
|
||||
int tearDownAll();
|
||||
int tearDownAll() override;
|
||||
|
||||
/**
|
||||
* @brief Check if an object exists
|
||||
* @param key Key to check
|
||||
* @return 1 if exists, 0 if not exists, -1 if error
|
||||
*/
|
||||
int isExist(const std::string &key);
|
||||
int isExist(const std::string& key) override;
|
||||
|
||||
/**
|
||||
* @brief Check if multiple objects exist
|
||||
|
|
@ -265,7 +263,7 @@ class RealClient : public PyClient {
|
|||
* @return Vector of existence results: 1 if exists, 0 if not exists, -1 if
|
||||
* error
|
||||
*/
|
||||
std::vector<int> batchIsExist(const std::vector<std::string> &keys);
|
||||
std::vector<int> batchIsExist(const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief Get the size of an object
|
||||
|
|
@ -273,35 +271,38 @@ class RealClient : public PyClient {
|
|||
* @return Size of the object in bytes, or -1 if error or object doesn't
|
||||
* exist
|
||||
*/
|
||||
int64_t getSize(const std::string &key);
|
||||
int64_t getSize(const std::string& key) override;
|
||||
|
||||
// Dummy client helper functions that return tl::expected
|
||||
tl::expected<std::tuple<uint64_t, size_t>, ErrorCode>
|
||||
get_buffer_info_dummy_helper(const std::string &key, const UUID &client_id);
|
||||
get_buffer_info_dummy_helper(const std::string& key,
|
||||
const ReadRouteConfig& config,
|
||||
const UUID& client_id);
|
||||
|
||||
tl::expected<void, ErrorCode> put_dummy_helper(
|
||||
const std::string &key, std::span<const char> value,
|
||||
const ReplicateConfig &config, const UUID &client_id);
|
||||
tl::expected<void, ErrorCode> put_dummy_helper(const std::string& key,
|
||||
std::span<const char> value,
|
||||
const WriteConfig& config,
|
||||
const UUID& client_id);
|
||||
|
||||
tl::expected<void, ErrorCode> put_batch_dummy_helper(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::span<const char>> &values,
|
||||
const ReplicateConfig &config, const UUID &client_id);
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::span<const char>>& values,
|
||||
const WriteConfig& config, const UUID& client_id);
|
||||
|
||||
tl::expected<void, ErrorCode> put_parts_dummy_helper(
|
||||
const std::string &key, std::vector<std::span<const char>> values,
|
||||
const ReplicateConfig &config, const UUID &client_id);
|
||||
const std::string& key, std::vector<std::span<const char>> values,
|
||||
const WriteConfig& config, const UUID& client_id);
|
||||
|
||||
std::vector<tl::expected<int64_t, ErrorCode>> batch_get_into_dummy_helper(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<uint64_t> &buffers, const std::vector<size_t> &sizes,
|
||||
const UUID &client_id);
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<uint64_t>& buffers, const std::vector<size_t>& sizes,
|
||||
const ReadRouteConfig& config, const UUID& client_id);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> batch_put_from_dummy_helper(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<uint64_t> &dummy_buffers,
|
||||
const std::vector<size_t> &sizes, const ReplicateConfig &config,
|
||||
const UUID &client_id);
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<uint64_t>& dummy_buffers,
|
||||
const std::vector<size_t>& sizes, const WriteConfig& config,
|
||||
const UUID& client_id);
|
||||
|
||||
// Share mem management for dummy client
|
||||
// Modified: map_shm_internal now takes fd instead of just name
|
||||
|
|
@ -309,145 +310,123 @@ class RealClient : public PyClient {
|
|||
uint64_t shm_base_addr,
|
||||
size_t shm_size,
|
||||
bool is_local_buffer,
|
||||
const UUID &client_id);
|
||||
const UUID& client_id);
|
||||
|
||||
tl::expected<void, ErrorCode> unmap_shm_internal(const UUID &client_id);
|
||||
tl::expected<void, ErrorCode> unmap_shm_internal(const UUID& client_id);
|
||||
|
||||
tl::expected<void, ErrorCode> unregister_shm_buffer_internal(
|
||||
uint64_t dummy_base_addr, const UUID &client_id);
|
||||
uint64_t dummy_base_addr, const UUID& client_id);
|
||||
|
||||
// Internal versions that return tl::expected
|
||||
tl::expected<void, ErrorCode> service_ready_internal() { return {}; }
|
||||
tl::expected<DeploymentMode, ErrorCode> service_ready_internal() {
|
||||
return deployment_mode();
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> setup_internal(
|
||||
const std::string &local_hostname, const std::string &metadata_server,
|
||||
size_t global_segment_size = 1024 * 1024 * 16,
|
||||
size_t local_buffer_size = 1024 * 1024 * 16,
|
||||
const std::string &protocol = "tcp",
|
||||
const std::string &rdma_devices = "",
|
||||
const std::string &master_server_addr = "127.0.0.1:50051",
|
||||
const std::shared_ptr<TransferEngine> &transfer_engine = nullptr,
|
||||
const std::string &ipc_socket_path = "", bool enable_offload = false);
|
||||
template <typename ConfigT>
|
||||
tl::expected<void, ErrorCode> setup_internal(ConfigT& config);
|
||||
|
||||
tl::expected<void, ErrorCode> initAll_internal(
|
||||
const std::string &protocol, const std::string &device_name,
|
||||
const std::string& protocol, const std::string& device_name,
|
||||
size_t mount_segment_size = 1024 * 1024 * 16);
|
||||
|
||||
tl::expected<void, ErrorCode> unregister_buffer_internal(void *buffer);
|
||||
tl::expected<void, ErrorCode> unregister_buffer_internal(void* buffer);
|
||||
|
||||
tl::expected<void, ErrorCode> put_internal(
|
||||
const std::string &key, std::span<const char> value,
|
||||
const ReplicateConfig &config = ReplicateConfig{},
|
||||
const std::string& key, std::span<const char> value,
|
||||
const WriteConfig& config,
|
||||
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator =
|
||||
nullptr);
|
||||
|
||||
tl::expected<void, ErrorCode> register_buffer_internal(void *buffer,
|
||||
tl::expected<void, ErrorCode> register_buffer_internal(void* buffer,
|
||||
size_t size);
|
||||
|
||||
tl::expected<int64_t, ErrorCode> get_into_internal(const std::string &key,
|
||||
void *buffer,
|
||||
size_t size);
|
||||
tl::expected<int64_t, ErrorCode> get_into_internal(
|
||||
const std::string& key, void* buffer, size_t size,
|
||||
const ReadRouteConfig& config = {});
|
||||
|
||||
std::vector<tl::expected<int64_t, ErrorCode>> batch_get_into_internal(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<void *> &buffers, const std::vector<size_t> &sizes);
|
||||
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes, const ReadRouteConfig& config = {});
|
||||
|
||||
std::vector<tl::expected<int64_t, ErrorCode>>
|
||||
batch_get_into_multi_buffers_internal(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::vector<void *>> &all_buffers,
|
||||
const std::vector<std::vector<size_t>> &all_sizes,
|
||||
bool prefer_same_node);
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
bool aggregate_same_segment_task, const ReadRouteConfig& config = {});
|
||||
|
||||
tl::expected<void, ErrorCode> put_from_internal(
|
||||
const std::string &key, void *buffer, size_t size,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
tl::expected<void, ErrorCode> put_from_internal(const std::string& key,
|
||||
void* buffer, size_t size,
|
||||
const WriteConfig& config);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> batch_put_from_internal(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<void *> &buffers, const std::vector<size_t> &sizes,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes, const WriteConfig& config);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>>
|
||||
batch_put_from_multi_buffers_internal(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::vector<void *>> &all_buffers,
|
||||
const std::vector<std::vector<size_t>> &all_sizes,
|
||||
const ReplicateConfig &config = ReplicateConfig{});
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffers,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
const WriteConfig& config);
|
||||
|
||||
tl::expected<void, ErrorCode> put_parts_internal(
|
||||
const std::string &key, std::vector<std::span<const char>> values,
|
||||
const ReplicateConfig &config = ReplicateConfig{},
|
||||
const std::string& key, std::vector<std::span<const char>> values,
|
||||
const WriteConfig& config,
|
||||
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator =
|
||||
nullptr);
|
||||
|
||||
tl::expected<void, ErrorCode> put_batch_internal(
|
||||
const std::vector<std::string> &keys,
|
||||
const std::vector<std::span<const char>> &values,
|
||||
const ReplicateConfig &config = ReplicateConfig{},
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::span<const char>>& values,
|
||||
const WriteConfig& config,
|
||||
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator =
|
||||
nullptr);
|
||||
|
||||
tl::expected<void, ErrorCode> remove_internal(const std::string &key);
|
||||
tl::expected<void, ErrorCode> remove_internal(const std::string& key);
|
||||
|
||||
tl::expected<long, ErrorCode> removeByRegex_internal(
|
||||
const std::string &str);
|
||||
const std::string& str);
|
||||
|
||||
tl::expected<int64_t, ErrorCode> removeAll_internal();
|
||||
|
||||
tl::expected<void, ErrorCode> tearDownAll_internal();
|
||||
|
||||
tl::expected<bool, ErrorCode> isExist_internal(const std::string &key);
|
||||
tl::expected<bool, ErrorCode> isExist_internal(const std::string& key);
|
||||
|
||||
std::vector<tl::expected<bool, ErrorCode>> batchIsExist_internal(
|
||||
const std::vector<std::string> &keys);
|
||||
const std::vector<std::string>& keys);
|
||||
|
||||
tl::expected<int64_t, ErrorCode> getSize_internal(const std::string &key);
|
||||
tl::expected<int64_t, ErrorCode> getSize_internal(const std::string& key);
|
||||
|
||||
std::shared_ptr<BufferHandle> get_buffer_internal(
|
||||
const std::string &key,
|
||||
const std::string& key,
|
||||
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator =
|
||||
nullptr);
|
||||
nullptr,
|
||||
const ReadRouteConfig& config = {});
|
||||
|
||||
std::vector<std::shared_ptr<BufferHandle>> batch_get_buffer_internal(
|
||||
const std::vector<std::string> &keys);
|
||||
const std::vector<std::string>& keys,
|
||||
const ReadRouteConfig& config = {});
|
||||
|
||||
std::map<std::string, std::vector<Replica::Descriptor>>
|
||||
batch_get_replica_desc(const std::vector<std::string> &keys);
|
||||
std::vector<Replica::Descriptor> get_replica_desc(const std::string &key);
|
||||
batch_get_replica_desc(const std::vector<std::string>& keys);
|
||||
std::vector<Replica::Descriptor> get_replica_desc(const std::string& key);
|
||||
|
||||
tl::expected<PingResponse, ErrorCode> ping(const UUID &client_id);
|
||||
tl::expected<HeartbeatResponse, ErrorCode> ping(const UUID& client_id);
|
||||
|
||||
std::unique_ptr<AutoPortBinder> port_binder_ = nullptr;
|
||||
|
||||
struct SegmentDeleter {
|
||||
void operator()(void *ptr) {
|
||||
if (ptr) {
|
||||
free(ptr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct AscendSegmentDeleter {
|
||||
void operator()(void *ptr) {
|
||||
if (ptr) {
|
||||
free_memory("ascend", ptr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<void, SegmentDeleter>> segment_ptrs_;
|
||||
std::vector<std::unique_ptr<void, AscendSegmentDeleter>>
|
||||
ascend_segment_ptrs_;
|
||||
std::string protocol;
|
||||
std::string device_name;
|
||||
std::string local_hostname;
|
||||
std::string local_ip;
|
||||
uint16_t te_port = 0;
|
||||
|
||||
struct MappedShm {
|
||||
std::string shm_name;
|
||||
// Offset = real_base - dummy_base
|
||||
uintptr_t shm_addr_offset = 0;
|
||||
void *shm_buffer = nullptr;
|
||||
void* shm_buffer = nullptr;
|
||||
size_t shm_size = 0;
|
||||
uintptr_t dummy_base_addr = 0;
|
||||
};
|
||||
|
|
@ -467,8 +446,11 @@ class RealClient : public PyClient {
|
|||
// Dummy Client manage related members
|
||||
void dummy_client_monitor_func();
|
||||
int start_dummy_client_monitor();
|
||||
int stop_dummy_client_monitor();
|
||||
std::thread dummy_client_monitor_thread_;
|
||||
std::atomic<bool> dummy_client_monitor_running_{false};
|
||||
std::condition_variable dummy_client_monitor_cv_;
|
||||
std::mutex dummy_client_monitor_cv_mutex_;
|
||||
static constexpr uint64_t kDummyClientMonitorSleepMs =
|
||||
1000; // 1000 ms sleep between client monitor checks
|
||||
// boost lockfree queue requires trivial assignment operator
|
||||
|
|
|
|||
|
|
@ -18,13 +18,16 @@
|
|||
|
||||
namespace mooncake {
|
||||
|
||||
class P2PClientMeta;
|
||||
|
||||
/**
|
||||
* @brief Type of buffer allocator used in the system
|
||||
*/
|
||||
enum class ReplicaType {
|
||||
MEMORY, // Memory replica
|
||||
DISK, // Disk replica
|
||||
LOCAL_DISK // Local disk replica
|
||||
MEMORY, // Memory replica
|
||||
DISK, // Disk replica
|
||||
LOCAL_DISK, // Local disk replica
|
||||
P2P_PROXY, // routing replica (only for P2P structure)
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -34,7 +37,9 @@ inline std::ostream& operator<<(std::ostream& os,
|
|||
const ReplicaType& replicaType) noexcept {
|
||||
static const std::unordered_map<ReplicaType, std::string_view>
|
||||
replica_type_strings{{ReplicaType::MEMORY, "MEMORY"},
|
||||
{ReplicaType::DISK, "DISK"}};
|
||||
{ReplicaType::DISK, "DISK"},
|
||||
{ReplicaType::LOCAL_DISK, "LOCAL_DISK"},
|
||||
{ReplicaType::P2P_PROXY, "P2P_PROXY"}};
|
||||
|
||||
os << (replica_type_strings.count(replicaType)
|
||||
? replica_type_strings.at(replicaType)
|
||||
|
|
@ -119,6 +124,19 @@ struct LocalDiskReplicaData {
|
|||
std::string transport_endpoint;
|
||||
};
|
||||
|
||||
struct P2PProxyReplicaData {
|
||||
P2PProxyReplicaData() = default;
|
||||
P2PProxyReplicaData(std::shared_ptr<P2PClientMeta> client,
|
||||
std::shared_ptr<Segment> segment, uint64_t object_size)
|
||||
: client(std::move(client)),
|
||||
segment(std::move(segment)),
|
||||
object_size(object_size) {}
|
||||
|
||||
std::shared_ptr<const P2PClientMeta> client;
|
||||
std::shared_ptr<const Segment> segment;
|
||||
uint64_t object_size = 0;
|
||||
};
|
||||
|
||||
struct MemoryDescriptor {
|
||||
AllocatedBuffer::Descriptor buffer_descriptor;
|
||||
YLT_REFL(MemoryDescriptor, buffer_descriptor);
|
||||
|
|
@ -137,6 +155,16 @@ struct LocalDiskDescriptor {
|
|||
YLT_REFL(LocalDiskDescriptor, client_id, object_size, transport_endpoint);
|
||||
};
|
||||
|
||||
struct P2PProxyDescriptor {
|
||||
UUID client_id;
|
||||
UUID segment_id;
|
||||
std::string ip_address;
|
||||
uint16_t rpc_port = 0;
|
||||
uint64_t object_size = 0;
|
||||
YLT_REFL(P2PProxyDescriptor, client_id, segment_id, ip_address, rpc_port,
|
||||
object_size);
|
||||
};
|
||||
|
||||
class Replica {
|
||||
public:
|
||||
struct Descriptor;
|
||||
|
|
@ -159,6 +187,9 @@ class Replica {
|
|||
std::move(transport_endpoint)}),
|
||||
status_(status) {}
|
||||
|
||||
Replica(P2PProxyReplicaData proxy_data, ReplicaStatus status)
|
||||
: data_(std::move(proxy_data)), status_(status) {}
|
||||
|
||||
~Replica() {
|
||||
if (status_ != ReplicaStatus::UNDEFINED && is_disk_replica()) {
|
||||
const auto& disk_data = std::get<DiskReplicaData>(data_);
|
||||
|
|
@ -220,6 +251,10 @@ class Replica {
|
|||
return std::holds_alternative<LocalDiskReplicaData>(data_);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_p2p_proxy_replica() const {
|
||||
return std::holds_alternative<P2PProxyReplicaData>(data_);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool has_invalid_mem_handle() const {
|
||||
if (is_memory_replica()) {
|
||||
const auto& mem_data = std::get<MemoryReplicaData>(data_);
|
||||
|
|
@ -241,6 +276,9 @@ class Replica {
|
|||
[[nodiscard]] std::vector<std::optional<std::string>> get_segment_names()
|
||||
const;
|
||||
|
||||
// only memory replica and p2p proxy replica have segment id
|
||||
[[nodiscard]] std::optional<UUID> get_segment_id() const;
|
||||
|
||||
void mark_complete() {
|
||||
if (status_ == ReplicaStatus::PROCESSING) {
|
||||
status_ = ReplicaStatus::COMPLETE;
|
||||
|
|
@ -251,6 +289,44 @@ class Replica {
|
|||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string>& get_p2p_tags() const {
|
||||
static const std::vector<std::string> empty_tags;
|
||||
auto segment = get_p2p_segment();
|
||||
if (segment && segment->IsP2PSegment()) {
|
||||
return segment->GetP2PExtra().tags;
|
||||
}
|
||||
return empty_tags;
|
||||
}
|
||||
|
||||
std::optional<int> get_p2p_priority() const {
|
||||
auto segment = get_p2p_segment();
|
||||
if (segment && segment->IsP2PSegment()) {
|
||||
return segment->GetP2PExtra().priority;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<MemoryType> get_p2p_memory_type() const {
|
||||
auto segment = get_p2p_segment();
|
||||
if (segment && segment->IsP2PSegment()) {
|
||||
return segment->GetP2PExtra().memory_type;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<UUID> get_p2p_client_id() const;
|
||||
|
||||
std::shared_ptr<const Segment> get_p2p_segment() const {
|
||||
if (!is_p2p_proxy_replica()) return nullptr;
|
||||
return std::get<P2PProxyReplicaData>(data_).segment;
|
||||
}
|
||||
|
||||
std::shared_ptr<const P2PClientMeta> get_p2p_client() const {
|
||||
if (!is_p2p_proxy_replica()) return nullptr;
|
||||
return std::get<P2PProxyReplicaData>(data_).client;
|
||||
}
|
||||
|
||||
public:
|
||||
friend std::ostream& operator<<(std::ostream& os, const Replica& replica);
|
||||
|
||||
struct ReplicaTypeVisitor {
|
||||
|
|
@ -263,15 +339,34 @@ class Replica {
|
|||
ReplicaType operator()(const LocalDiskReplicaData&) const {
|
||||
return ReplicaType::LOCAL_DISK;
|
||||
}
|
||||
ReplicaType operator()(const P2PProxyReplicaData&) const {
|
||||
return ReplicaType::P2P_PROXY;
|
||||
}
|
||||
};
|
||||
|
||||
struct Descriptor {
|
||||
std::variant<MemoryDescriptor, DiskDescriptor, LocalDiskDescriptor>
|
||||
std::variant<MemoryDescriptor, DiskDescriptor, LocalDiskDescriptor,
|
||||
P2PProxyDescriptor>
|
||||
descriptor_variant;
|
||||
ReplicaStatus status;
|
||||
YLT_REFL(Descriptor, descriptor_variant, status);
|
||||
|
||||
// Helper functions
|
||||
ReplicaType type() const {
|
||||
return std::visit(
|
||||
[](const auto& desc) -> ReplicaType {
|
||||
using T = std::decay_t<decltype(desc)>;
|
||||
if constexpr (std::is_same_v<T, MemoryDescriptor>)
|
||||
return ReplicaType::MEMORY;
|
||||
else if constexpr (std::is_same_v<T, DiskDescriptor>)
|
||||
return ReplicaType::DISK;
|
||||
else if constexpr (std::is_same_v<T, LocalDiskDescriptor>)
|
||||
return ReplicaType::LOCAL_DISK;
|
||||
else
|
||||
return ReplicaType::P2P_PROXY;
|
||||
},
|
||||
descriptor_variant);
|
||||
}
|
||||
bool is_memory_replica() noexcept {
|
||||
return std::holds_alternative<MemoryDescriptor>(descriptor_variant);
|
||||
}
|
||||
|
|
@ -298,6 +393,16 @@ class Replica {
|
|||
descriptor_variant);
|
||||
}
|
||||
|
||||
bool is_p2p_proxy_replica() noexcept {
|
||||
return std::holds_alternative<P2PProxyDescriptor>(
|
||||
descriptor_variant);
|
||||
}
|
||||
|
||||
bool is_p2p_proxy_replica() const noexcept {
|
||||
return std::holds_alternative<P2PProxyDescriptor>(
|
||||
descriptor_variant);
|
||||
}
|
||||
|
||||
MemoryDescriptor& get_memory_descriptor() {
|
||||
if (auto* desc =
|
||||
std::get_if<MemoryDescriptor>(&descriptor_variant)) {
|
||||
|
|
@ -343,48 +448,34 @@ class Replica {
|
|||
}
|
||||
throw std::runtime_error("Expected LocalDiskDescriptor");
|
||||
}
|
||||
|
||||
P2PProxyDescriptor& get_p2p_proxy_descriptor() {
|
||||
if (auto* desc =
|
||||
std::get_if<P2PProxyDescriptor>(&descriptor_variant)) {
|
||||
return *desc;
|
||||
}
|
||||
throw std::runtime_error("Expected P2PProxyDescriptor");
|
||||
}
|
||||
|
||||
const P2PProxyDescriptor& get_p2p_proxy_descriptor() const {
|
||||
if (auto* desc =
|
||||
std::get_if<P2PProxyDescriptor>(&descriptor_variant)) {
|
||||
return *desc;
|
||||
}
|
||||
throw std::runtime_error("Expected P2PProxyDescriptor");
|
||||
}
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os,
|
||||
const Descriptor& desc);
|
||||
};
|
||||
|
||||
private:
|
||||
std::variant<MemoryReplicaData, DiskReplicaData, LocalDiskReplicaData>
|
||||
std::variant<MemoryReplicaData, DiskReplicaData, LocalDiskReplicaData,
|
||||
P2PProxyReplicaData>
|
||||
data_;
|
||||
ReplicaStatus status_{ReplicaStatus::UNDEFINED};
|
||||
};
|
||||
|
||||
inline Replica::Descriptor Replica::get_descriptor() const {
|
||||
Replica::Descriptor desc;
|
||||
desc.status = status_;
|
||||
|
||||
if (is_memory_replica()) {
|
||||
const auto& mem_data = std::get<MemoryReplicaData>(data_);
|
||||
MemoryDescriptor mem_desc;
|
||||
if (mem_data.buffer) {
|
||||
mem_desc.buffer_descriptor = mem_data.buffer->get_descriptor();
|
||||
} else {
|
||||
mem_desc.buffer_descriptor.size_ = 0;
|
||||
mem_desc.buffer_descriptor.buffer_address_ = 0;
|
||||
mem_desc.buffer_descriptor.transport_endpoint_ = "";
|
||||
LOG(ERROR) << "Trying to get invalid memory replica descriptor";
|
||||
}
|
||||
desc.descriptor_variant = std::move(mem_desc);
|
||||
} else if (is_disk_replica()) {
|
||||
const auto& disk_data = std::get<DiskReplicaData>(data_);
|
||||
DiskDescriptor disk_desc;
|
||||
disk_desc.file_path = disk_data.file_path;
|
||||
disk_desc.object_size = disk_data.object_size;
|
||||
desc.descriptor_variant = std::move(disk_desc);
|
||||
} else if (is_local_disk_replica()) {
|
||||
const auto& disk_data = std::get<LocalDiskReplicaData>(data_);
|
||||
LocalDiskDescriptor local_disk_desc;
|
||||
local_disk_desc.client_id = disk_data.client_id;
|
||||
local_disk_desc.object_size = disk_data.object_size;
|
||||
local_disk_desc.transport_endpoint = disk_data.transport_endpoint;
|
||||
desc.descriptor_variant = std::move(local_disk_desc);
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
inline std::vector<std::optional<std::string>> Replica::get_segment_names()
|
||||
const {
|
||||
if (is_memory_replica()) {
|
||||
|
|
@ -400,24 +491,21 @@ inline std::vector<std::optional<std::string>> Replica::get_segment_names()
|
|||
return std::vector<std::optional<std::string>>();
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const Replica& replica) {
|
||||
os << "Replica: { status: " << replica.status_ << ", ";
|
||||
|
||||
if (replica.is_memory_replica()) {
|
||||
const auto& mem_data = std::get<MemoryReplicaData>(replica.data_);
|
||||
os << "type: MEMORY, buffers: [";
|
||||
inline std::optional<UUID> Replica::get_segment_id() const {
|
||||
if (is_memory_replica()) {
|
||||
const auto& mem_data = std::get<MemoryReplicaData>(data_);
|
||||
if (mem_data.buffer) {
|
||||
os << *mem_data.buffer;
|
||||
return mem_data.buffer->getSegmentId();
|
||||
}
|
||||
} else if (is_p2p_proxy_replica()) {
|
||||
const auto& proxy_data = std::get<P2PProxyReplicaData>(data_);
|
||||
if (proxy_data.segment) {
|
||||
return proxy_data.segment->id;
|
||||
}
|
||||
os << "]";
|
||||
} else if (replica.is_disk_replica()) {
|
||||
const auto& disk_data = std::get<DiskReplicaData>(replica.data_);
|
||||
os << "type: DISK, file_path: " << disk_data.file_path
|
||||
<< ", object_size: " << disk_data.object_size;
|
||||
}
|
||||
|
||||
os << " }";
|
||||
return os;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const Replica& replica);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
|
|||
|
|
@ -0,0 +1,379 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include "replica.h"
|
||||
#include "offset_allocator/offset_allocator.hpp"
|
||||
#include "mutex.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @brief Memory-efficient, flat storage for P2P replica routes.
|
||||
*/
|
||||
struct P2PRouteData {
|
||||
struct Item {
|
||||
UUID client_id;
|
||||
UUID segment_id;
|
||||
char ip_address[48];
|
||||
uint16_t rpc_port;
|
||||
uint64_t object_size;
|
||||
};
|
||||
size_t count;
|
||||
// items follow count in memory
|
||||
|
||||
[[nodiscard]] const Item* data() const {
|
||||
return reinterpret_cast<const Item*>(
|
||||
reinterpret_cast<const char*>(this) + sizeof(size_t));
|
||||
}
|
||||
|
||||
[[nodiscard]] Item* data() {
|
||||
return reinterpret_cast<Item*>(reinterpret_cast<char*>(this) +
|
||||
sizeof(size_t));
|
||||
}
|
||||
|
||||
static size_t CalculateSize(size_t replica_count) {
|
||||
// Ensure 8-byte alignment for the entire record
|
||||
return (sizeof(size_t) + replica_count * sizeof(Item) + 7) & ~7;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Serialize replica data and key into a continuous memory block.
|
||||
* @return Total bytes written.
|
||||
*/
|
||||
static size_t Serialize(void* dest, const std::string& key,
|
||||
const std::vector<P2PProxyDescriptor>& replicas);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief High-performance read-only handle for RouteCache entries.
|
||||
*
|
||||
* Holds a shared_ptr to the underlying OffsetAllocationHandle, ensuring the
|
||||
* data memory remains valid as long as this handle exists — even after the
|
||||
* Node is recycled by the epoch-based reclamation system.
|
||||
*/
|
||||
class P2PRouteHandle {
|
||||
public:
|
||||
P2PRouteHandle() : record_(nullptr) {}
|
||||
P2PRouteHandle(
|
||||
const P2PRouteData* record,
|
||||
std::shared_ptr<offset_allocator::OffsetAllocationHandle> handle)
|
||||
: record_(record), handle_(std::move(handle)) {}
|
||||
|
||||
[[nodiscard]] std::span<const P2PRouteData::Item> items() const {
|
||||
if (!record_ || record_->count == 0) return {};
|
||||
return {record_->data(), record_->count};
|
||||
}
|
||||
|
||||
private:
|
||||
const P2PRouteData* record_;
|
||||
std::shared_ptr<offset_allocator::OffsetAllocationHandle> handle_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Client-side route cache for P2P read operations.
|
||||
*
|
||||
* This implementation uses an Atomic Bucket Array with Epoch-Based Reclamation
|
||||
* (EBR) to achieve lock-free reads while maintaining consistency under
|
||||
* concurrent writes. It uses a Clock algorithm (Second Chance) for O(1)
|
||||
* eviction.
|
||||
*
|
||||
* PROTECTION MODEL:
|
||||
* 1. [Node Metadata]: Protected by EBR. Readers enter an epoch guard before
|
||||
* traversing bucket chains. Nodes are only recycled after all readers that
|
||||
* observed the node have left their epoch — providing a deterministic
|
||||
* safety guarantee
|
||||
* 2. [Data Memory]: Protected by shared_ptr within P2PRouteHandle. Even after
|
||||
* the Node is recycled, the underlying P2PRouteData remains valid as long
|
||||
* as the caller holds the handle.
|
||||
*/
|
||||
class RouteCache {
|
||||
public:
|
||||
RouteCache(size_t max_memory_bytes, uint64_t ttl_ms);
|
||||
~RouteCache();
|
||||
|
||||
/**
|
||||
* @brief Lock-free lookup protected by EBR epoch guard.
|
||||
*/
|
||||
P2PRouteHandle Get(const std::string& key);
|
||||
|
||||
/**
|
||||
* @brief overwrite
|
||||
*/
|
||||
void Replace(const std::string& key,
|
||||
const std::vector<P2PProxyDescriptor>& replicas);
|
||||
|
||||
/**
|
||||
* @brief update if key exists, otherwise insert
|
||||
*/
|
||||
void Upsert(const std::string& key,
|
||||
const std::vector<P2PProxyDescriptor>& replicas);
|
||||
|
||||
void RemoveReplica(const std::string& key,
|
||||
const std::vector<P2PProxyDescriptor>& remove_replicas);
|
||||
|
||||
struct Metrics {
|
||||
size_t free_node_count;
|
||||
size_t total_node_count;
|
||||
size_t free_memory_bytes;
|
||||
size_t total_memory_bytes;
|
||||
};
|
||||
|
||||
Metrics GetMetrics() const;
|
||||
|
||||
private:
|
||||
// ========================================================================
|
||||
// Epoch-Based Reclamation (EBR) Infrastructure
|
||||
// ========================================================================
|
||||
|
||||
static constexpr uint64_t EPOCH_INACTIVE = UINT64_MAX;
|
||||
static constexpr size_t MAX_READER_SLOTS = 256;
|
||||
|
||||
struct alignas(64) ReaderSlot {
|
||||
std::atomic<uint64_t> epoch{EPOCH_INACTIVE};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief RAII guard that pins the current epoch for the calling thread.
|
||||
*
|
||||
* While the guard is alive, SyncGC will not recycle any Node that was
|
||||
* retired in or after the pinned epoch. This ensures safe lock-free
|
||||
* traversal of bucket chains in Get() and RemoveReplica().
|
||||
*/
|
||||
class EpochGuard {
|
||||
public:
|
||||
explicit EpochGuard(RouteCache* cache) : cache_(cache) {
|
||||
slot_ = GetReaderSlot();
|
||||
cache_->reader_slots_[slot_].epoch.store(
|
||||
cache_->global_epoch_.load(std::memory_order_acquire),
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
~EpochGuard() {
|
||||
cache_->reader_slots_[slot_].epoch.store(EPOCH_INACTIVE,
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
EpochGuard(const EpochGuard&) = delete;
|
||||
EpochGuard& operator=(const EpochGuard&) = delete;
|
||||
|
||||
private:
|
||||
static size_t GetReaderSlot() {
|
||||
static thread_local size_t slot =
|
||||
std::hash<std::thread::id>{}(std::this_thread::get_id()) %
|
||||
MAX_READER_SLOTS;
|
||||
return slot;
|
||||
}
|
||||
|
||||
RouteCache* cache_;
|
||||
size_t slot_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Compute the safe epoch for reclamation.
|
||||
*
|
||||
* Returns the maximum epoch E such that no active reader is in epoch <= E.
|
||||
* Nodes retired at epoch <= E can be safely recycled.
|
||||
*/
|
||||
uint64_t ComputeSafeEpoch() const;
|
||||
|
||||
std::atomic<uint64_t> global_epoch_{0};
|
||||
std::unique_ptr<ReaderSlot[]> reader_slots_;
|
||||
|
||||
// ========================================================================
|
||||
// Node & Shard Structures
|
||||
// ========================================================================
|
||||
|
||||
struct alignas(64) Node {
|
||||
const char* key_;
|
||||
uint32_t key_len_;
|
||||
std::atomic<bool> accessed_{false};
|
||||
std::atomic<bool> is_deleted_{false};
|
||||
|
||||
std::shared_ptr<offset_allocator::OffsetAllocationHandle> handle_;
|
||||
std::atomic<int64_t> deadline_;
|
||||
std::atomic<Node*> next_;
|
||||
|
||||
// Hash of the full key for fast bucket location during eviction
|
||||
uint32_t key_hash_{0};
|
||||
|
||||
Node() : key_(nullptr), key_len_(0), next_(nullptr) {}
|
||||
|
||||
void Init(
|
||||
const char* key, uint32_t key_len, size_t key_hash,
|
||||
std::shared_ptr<offset_allocator::OffsetAllocationHandle> handle,
|
||||
int64_t deadline_count) {
|
||||
key_ = key;
|
||||
key_len_ = key_len;
|
||||
key_hash_ = (uint32_t)key_hash;
|
||||
handle_ = std::move(handle);
|
||||
deadline_.store(deadline_count, std::memory_order_relaxed);
|
||||
next_.store(nullptr, std::memory_order_relaxed);
|
||||
accessed_.store(false, std::memory_order_relaxed);
|
||||
is_deleted_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read handle_.
|
||||
* Caller MUST be within an EpochGuard or holding the shard mutex.
|
||||
*/
|
||||
std::shared_ptr<offset_allocator::OffsetAllocationHandle> GetHandle()
|
||||
const {
|
||||
return handle_;
|
||||
}
|
||||
|
||||
void MarkDeleted() {
|
||||
is_deleted_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool IsDeleted() const {
|
||||
return is_deleted_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool IsExpired(int64_t now_count) const {
|
||||
return now_count >= deadline_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool IsActive(int64_t now) { return !IsDeleted() && !IsExpired(now); }
|
||||
};
|
||||
|
||||
struct alignas(64) Shard {
|
||||
mutable Mutex mtx_;
|
||||
std::unique_ptr<std::atomic<Node*>[]> buckets_ GUARDED_BY(mtx_);
|
||||
|
||||
// Clock Eviction scanning cursor
|
||||
std::atomic<size_t> evict_cursor_{0};
|
||||
|
||||
// Shard-local Node pool manager
|
||||
struct NodeResourcePool {
|
||||
std::unique_ptr<Node[]> storage_;
|
||||
Node* free_head_{nullptr};
|
||||
size_t free_count_{0};
|
||||
|
||||
void Init(size_t count) {
|
||||
storage_ = std::make_unique<Node[]>(count);
|
||||
Node* prev = nullptr;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
storage_[i].next_.store(prev, std::memory_order_relaxed);
|
||||
prev = &storage_[i];
|
||||
}
|
||||
free_head_ = prev;
|
||||
free_count_ = count;
|
||||
}
|
||||
|
||||
Node* Pop() {
|
||||
if (!free_head_) return nullptr;
|
||||
Node* node = free_head_;
|
||||
free_head_ = node->next_.load(std::memory_order_relaxed);
|
||||
free_count_--;
|
||||
return node;
|
||||
}
|
||||
|
||||
void Push(Node* node) {
|
||||
if (!node) return;
|
||||
node->next_.store(free_head_, std::memory_order_relaxed);
|
||||
free_head_ = node;
|
||||
free_count_++;
|
||||
}
|
||||
} nodes_ GUARDED_BY(mtx_);
|
||||
|
||||
// EBR Garbage Collection Queue
|
||||
struct PendingDelete {
|
||||
Node* node_;
|
||||
uint64_t retire_epoch_;
|
||||
};
|
||||
std::vector<PendingDelete> pending_deletes_ GUARDED_BY(mtx_);
|
||||
|
||||
std::shared_ptr<offset_allocator::OffsetAllocator> allocator_;
|
||||
void* base_addr_ = nullptr;
|
||||
size_t gc_skip_count_ = 0;
|
||||
|
||||
Shard() = default;
|
||||
// Non-copyable & Non-movable (due to mutex and arrays)
|
||||
Shard(const Shard&) = delete;
|
||||
Shard& operator=(const Shard&) = delete;
|
||||
Shard(Shard&&) = delete;
|
||||
Shard& operator=(Shard&&) = delete;
|
||||
};
|
||||
|
||||
private:
|
||||
void InnerPut(Shard& shard, size_t bucket_idx, size_t hash_val,
|
||||
const std::string& key,
|
||||
const std::vector<P2PProxyDescriptor>& replicas, bool merge);
|
||||
|
||||
void BuildReplicaList(
|
||||
Node* old_node,
|
||||
const std::vector<P2PProxyDescriptor>& increment_replicas,
|
||||
const std::vector<P2PProxyDescriptor>& remove_replicas,
|
||||
std::vector<P2PProxyDescriptor>& out);
|
||||
|
||||
size_t Evict(Shard& shard, size_t goal_free_count) REQUIRES(shard.mtx_);
|
||||
void GCLoop();
|
||||
void SyncGC(Shard& shard) REQUIRES(shard.mtx_);
|
||||
|
||||
// Internal Helpers
|
||||
|
||||
// Returns {prev, node} for a given key in the bucket.
|
||||
std::pair<Node*, Node*> findNodeInBucket(Shard& shard, size_t bucket_idx,
|
||||
const char* key_ptr,
|
||||
uint32_t key_len);
|
||||
|
||||
void retireNode(Shard& shard, Node* prev, Node* node, size_t bucket_idx)
|
||||
REQUIRES(shard.mtx_);
|
||||
|
||||
bool acquireResource(
|
||||
Shard& shard, size_t total_size, Node** out_node,
|
||||
std::optional<offset_allocator::OffsetAllocationHandle>& out_handle)
|
||||
REQUIRES(shard.mtx_);
|
||||
|
||||
private:
|
||||
// Estimation of memory cost for dynamic resource allocation
|
||||
static constexpr size_t AVG_REPLICA_COUNT = 1;
|
||||
static constexpr size_t AVG_KEY_LEN = 64;
|
||||
static constexpr size_t ENTRY_METADATA_COST =
|
||||
sizeof(Node) + sizeof(std::atomic<Node*>);
|
||||
static constexpr size_t ENTRY_DATA_COST =
|
||||
sizeof(P2PRouteData) +
|
||||
(AVG_REPLICA_COUNT * sizeof(P2PRouteData::Item)) + AVG_KEY_LEN;
|
||||
static constexpr size_t TOTAL_AVG_COST_PER_ENTRY =
|
||||
ENTRY_METADATA_COST + ENTRY_DATA_COST;
|
||||
|
||||
const size_t max_memory_bytes_;
|
||||
const uint64_t ttl_ms_;
|
||||
|
||||
size_t shard_count_;
|
||||
size_t nodes_per_shard_;
|
||||
size_t buckets_per_shard_;
|
||||
|
||||
static constexpr int MAX_TRY_LOCK_RETRIES = 5;
|
||||
|
||||
// GC Control
|
||||
static constexpr size_t MAX_GC_SKIP_COUNT = 5;
|
||||
static constexpr auto GC_IDLE_SLEEP = std::chrono::milliseconds(2000);
|
||||
static constexpr auto GC_LOW_PRESSURE_SLEEP =
|
||||
std::chrono::milliseconds(300);
|
||||
static constexpr auto GC_HIGH_PRESSURE_SLEEP =
|
||||
std::chrono::milliseconds(50);
|
||||
static constexpr double LOW_WATERMARK = 0.7;
|
||||
static constexpr double HIGH_WATERMARK = 0.9;
|
||||
static constexpr double ASYNC_EVICT_PROPORTION = 0.05;
|
||||
static constexpr size_t SYNC_EVICT_BATCH_SIZE = 5;
|
||||
|
||||
std::atomic<bool> stop_gc_{false};
|
||||
std::thread gc_thread_;
|
||||
|
||||
void* base_all_ = nullptr;
|
||||
std::vector<std::unique_ptr<Shard>> shards_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -15,13 +15,13 @@
|
|||
|
||||
namespace mooncake {
|
||||
|
||||
extern const uint64_t kMetricReportIntervalSeconds;
|
||||
static const uint64_t kMetricReportIntervalSeconds = 10;
|
||||
|
||||
class WrappedMasterService {
|
||||
public:
|
||||
WrappedMasterService(const WrappedMasterServiceConfig& config);
|
||||
|
||||
~WrappedMasterService();
|
||||
virtual ~WrappedMasterService();
|
||||
|
||||
void init_http_server();
|
||||
|
||||
|
|
@ -38,43 +38,19 @@ class WrappedMasterService {
|
|||
ErrorCode>
|
||||
BatchQueryIp(const std::vector<UUID>& client_ids);
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode> BatchReplicaClear(
|
||||
const std::vector<std::string>& object_keys, const UUID& client_id,
|
||||
const std::string& segment_name);
|
||||
|
||||
tl::expected<
|
||||
std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
|
||||
ErrorCode>
|
||||
GetReplicaListByRegex(const std::string& str);
|
||||
|
||||
tl::expected<GetReplicaListResponse, ErrorCode> GetReplicaList(
|
||||
const std::string& key);
|
||||
const std::string& key, const GetReplicaListRequestConfig& config =
|
||||
GetReplicaListRequestConfig());
|
||||
|
||||
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
|
||||
BatchGetReplicaList(const std::vector<std::string>& keys);
|
||||
|
||||
tl::expected<std::vector<Replica::Descriptor>, ErrorCode> PutStart(
|
||||
const UUID& client_id, const std::string& key,
|
||||
const uint64_t slice_length, const ReplicateConfig& config);
|
||||
|
||||
tl::expected<void, ErrorCode> PutEnd(const UUID& client_id,
|
||||
const std::string& key,
|
||||
ReplicaType replica_type);
|
||||
|
||||
tl::expected<void, ErrorCode> PutRevoke(const UUID& client_id,
|
||||
const std::string& key,
|
||||
ReplicaType replica_type);
|
||||
|
||||
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
||||
BatchPutStart(const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<uint64_t>& slice_lengths,
|
||||
const ReplicateConfig& config);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
|
||||
const UUID& client_id, const std::vector<std::string>& keys);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
|
||||
const UUID& client_id, const std::vector<std::string>& keys);
|
||||
BatchGetReplicaList(const std::vector<std::string>& keys,
|
||||
const GetReplicaListRequestConfig& config =
|
||||
GetReplicaListRequestConfig());
|
||||
|
||||
tl::expected<void, ErrorCode> Remove(const std::string& key);
|
||||
|
||||
|
|
@ -82,35 +58,27 @@ class WrappedMasterService {
|
|||
|
||||
long RemoveAll();
|
||||
|
||||
tl::expected<void, ErrorCode> MountSegment(const Segment& segment,
|
||||
const UUID& client_id);
|
||||
|
||||
tl::expected<void, ErrorCode> ReMountSegment(
|
||||
const std::vector<Segment>& segments, const UUID& client_id);
|
||||
|
||||
tl::expected<void, ErrorCode> UnmountSegment(const UUID& segment_id,
|
||||
const UUID& client_id);
|
||||
|
||||
tl::expected<std::string, ErrorCode> GetFsdir();
|
||||
tl::expected<void, ErrorCode> MountSegment(const Segment& segment,
|
||||
const UUID& client_id);
|
||||
|
||||
tl::expected<GetStorageConfigResponse, ErrorCode> GetStorageConfig();
|
||||
tl::expected<HeartbeatResponse, ErrorCode> Heartbeat(
|
||||
const HeartbeatRequest& req);
|
||||
|
||||
tl::expected<PingResponse, ErrorCode> Ping(const UUID& client_id);
|
||||
tl::expected<QueryClientStatusResponse, ErrorCode> QueryClientStatus(
|
||||
const QueryClientStatusRequest& req);
|
||||
|
||||
tl::expected<RegisterClientResponse, ErrorCode> RegisterClient(
|
||||
const RegisterClientRequest& req);
|
||||
|
||||
tl::expected<std::string, ErrorCode> ServiceReady();
|
||||
|
||||
tl::expected<void, ErrorCode> MountLocalDiskSegment(const UUID& client_id,
|
||||
bool enable_offloading);
|
||||
protected:
|
||||
virtual MasterService& GetMasterService() = 0;
|
||||
|
||||
tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>
|
||||
OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading);
|
||||
|
||||
tl::expected<void, ErrorCode> NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas);
|
||||
|
||||
private:
|
||||
MasterService master_service_;
|
||||
protected:
|
||||
std::thread metric_report_thread_;
|
||||
coro_http::coro_http_server http_server_;
|
||||
std::atomic<bool> metric_report_running_;
|
||||
|
|
|
|||
|
|
@ -2,43 +2,65 @@
|
|||
|
||||
#include "types.h"
|
||||
#include "replica.h"
|
||||
#include "heartbeat_type.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @brief Response structure for Ping operation
|
||||
* @brief P2P specific configuration for read route
|
||||
*/
|
||||
struct PingResponse {
|
||||
ViewVersionId view_version_id;
|
||||
ClientStatus client_status;
|
||||
|
||||
PingResponse() = default;
|
||||
PingResponse(ViewVersionId view_version, ClientStatus status)
|
||||
: view_version_id(view_version), client_status(status) {}
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os,
|
||||
const PingResponse& response) noexcept {
|
||||
return os << "PingResponse: { view_version_id: "
|
||||
<< response.view_version_id
|
||||
<< ", client_status: " << response.client_status << " }";
|
||||
}
|
||||
struct P2PGetReplicaListConfigExtra {
|
||||
// exclude replicas whose segment contains any tag in tag_filters
|
||||
std::vector<std::string> tag_filters;
|
||||
// filter replicas whose segment priority is lower than priority_limit
|
||||
int priority_limit = 0;
|
||||
};
|
||||
YLT_REFL(PingResponse, view_version_id, client_status);
|
||||
YLT_REFL(P2PGetReplicaListConfigExtra, tag_filters, priority_limit);
|
||||
|
||||
/**
|
||||
* @brief Request config for getting replica list
|
||||
*/
|
||||
struct GetReplicaListRequestConfig {
|
||||
GetReplicaListRequestConfig() = default;
|
||||
GetReplicaListRequestConfig(size_t max_c) : max_candidates(max_c) {}
|
||||
|
||||
// 0 means return all viable replica candidates;
|
||||
// otherwise, return at most max_candidates candidates
|
||||
static const size_t RETURN_ALL_CANDIDATES = 0;
|
||||
size_t max_candidates = RETURN_ALL_CANDIDATES;
|
||||
std::optional<P2PGetReplicaListConfigExtra> p2p_config;
|
||||
};
|
||||
YLT_REFL(GetReplicaListRequestConfig, max_candidates, p2p_config);
|
||||
|
||||
// config for filter replicas in read route
|
||||
typedef GetReplicaListRequestConfig ReadRouteConfig;
|
||||
typedef P2PGetReplicaListConfigExtra P2PReadRouteConfigExtra;
|
||||
|
||||
/**
|
||||
* @brief Extra info for centralized read route response (Internal use)
|
||||
*/
|
||||
struct CentralizedGetReplicaListResponseExtra {
|
||||
CentralizedGetReplicaListResponseExtra() = default;
|
||||
CentralizedGetReplicaListResponseExtra(uint64_t lease_ttl_ms_param)
|
||||
: lease_ttl_ms(lease_ttl_ms_param) {}
|
||||
uint64_t lease_ttl_ms = 0;
|
||||
};
|
||||
YLT_REFL(CentralizedGetReplicaListResponseExtra, lease_ttl_ms);
|
||||
|
||||
/**
|
||||
* @brief Response structure for GetReplicaList operation
|
||||
*/
|
||||
struct GetReplicaListResponse {
|
||||
std::vector<Replica::Descriptor> replicas;
|
||||
uint64_t lease_ttl_ms;
|
||||
|
||||
GetReplicaListResponse() : lease_ttl_ms(0) {}
|
||||
GetReplicaListResponse() = default;
|
||||
GetReplicaListResponse(std::vector<Replica::Descriptor>&& replicas_param,
|
||||
uint64_t lease_ttl_ms_param)
|
||||
: replicas(std::move(replicas_param)),
|
||||
lease_ttl_ms(lease_ttl_ms_param) {}
|
||||
centralized_extra(lease_ttl_ms_param) {}
|
||||
|
||||
std::vector<Replica::Descriptor> replicas;
|
||||
std::optional<CentralizedGetReplicaListResponseExtra> centralized_extra;
|
||||
};
|
||||
YLT_REFL(GetReplicaListResponse, replicas, lease_ttl_ms);
|
||||
YLT_REFL(GetReplicaListResponse, replicas, centralized_extra);
|
||||
|
||||
/**
|
||||
* @brief Response structure for GetStorageConfig operation
|
||||
|
|
@ -56,4 +78,69 @@ struct GetStorageConfigResponse {
|
|||
quota_bytes(quota) {}
|
||||
};
|
||||
YLT_REFL(GetStorageConfigResponse, fsdir, enable_disk_eviction, quota_bytes);
|
||||
|
||||
/**
|
||||
* @brief Request structure for Heartbeat operation.
|
||||
* Client could set HeartbeatTasks for Master to run
|
||||
*/
|
||||
struct HeartbeatRequest {
|
||||
UUID client_id;
|
||||
std::vector<HeartbeatTask> tasks;
|
||||
};
|
||||
YLT_REFL(HeartbeatRequest, client_id, tasks);
|
||||
|
||||
/**
|
||||
* @brief Response structure for Heartbeat operation.
|
||||
* Always returns view_version; client uses it under UNDEFINED status
|
||||
* for crash-recovery decisions, other statuses for defensive checks.
|
||||
*/
|
||||
struct HeartbeatResponse {
|
||||
ClientStatus status;
|
||||
ViewVersionId view_version = 0;
|
||||
std::vector<HeartbeatTaskResult> task_results;
|
||||
};
|
||||
YLT_REFL(HeartbeatResponse, status, view_version, task_results);
|
||||
|
||||
/**
|
||||
* @brief Request structure for RegisterClient operation.
|
||||
* Client calls this on startup to register its UUID and local segments.
|
||||
* P2P clients additionally provide ip_address and rpc_port.
|
||||
*/
|
||||
struct RegisterClientRequest {
|
||||
UUID client_id;
|
||||
std::vector<Segment> segments;
|
||||
DeploymentMode deployment_mode = DeploymentMode::CENTRALIZATION;
|
||||
|
||||
// P2P only: network endpoint info
|
||||
std::optional<std::string> ip_address;
|
||||
std::optional<uint16_t> rpc_port;
|
||||
};
|
||||
YLT_REFL(RegisterClientRequest, client_id, segments, deployment_mode,
|
||||
ip_address, rpc_port);
|
||||
|
||||
/**
|
||||
* @brief Response structure for RegisterClient operation.
|
||||
* Returns the master's view_version to client for crash checking.
|
||||
*/
|
||||
struct RegisterClientResponse {
|
||||
ViewVersionId view_version = 0;
|
||||
};
|
||||
YLT_REFL(RegisterClientResponse, view_version);
|
||||
|
||||
/**
|
||||
* @brief Request structure for QueryClientStatus operation.
|
||||
*/
|
||||
struct QueryClientStatusRequest {
|
||||
UUID client_id;
|
||||
};
|
||||
YLT_REFL(QueryClientStatusRequest, client_id);
|
||||
|
||||
/**
|
||||
* @brief Response structure for QueryClientStatus operation.
|
||||
*/
|
||||
struct QueryClientStatusResponse {
|
||||
ClientStatus status = ClientStatus::UNDEFINED;
|
||||
};
|
||||
YLT_REFL(QueryClientStatusResponse, status);
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <ostream>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "allocation_strategy.h"
|
||||
#include "allocator.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
/**
|
||||
* @brief Status of a mounted segment in master
|
||||
*/
|
||||
enum class SegmentStatus {
|
||||
UNDEFINED = 0, // Uninitialized
|
||||
OK, // Segment is mounted and available for allocation
|
||||
UNMOUNTING, // Segment is under unmounting
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stream operator for SegmentStatus
|
||||
*/
|
||||
inline std::ostream& operator<<(std::ostream& os,
|
||||
const SegmentStatus& status) noexcept {
|
||||
static const std::unordered_map<SegmentStatus, std::string_view>
|
||||
status_strings{{SegmentStatus::UNDEFINED, "UNDEFINED"},
|
||||
{SegmentStatus::OK, "OK"},
|
||||
{SegmentStatus::UNMOUNTING, "UNMOUNTING"}};
|
||||
|
||||
os << (status_strings.count(status) ? status_strings.at(status)
|
||||
: "UNKNOWN");
|
||||
return os;
|
||||
}
|
||||
|
||||
struct MountedSegment {
|
||||
Segment segment;
|
||||
SegmentStatus status;
|
||||
std::shared_ptr<BufferAllocatorBase> buf_allocator;
|
||||
};
|
||||
|
||||
struct LocalDiskSegment {
|
||||
mutable Mutex offloading_mutex_;
|
||||
bool enable_offloading;
|
||||
std::unordered_map<std::string, int64_t> GUARDED_BY(offloading_mutex_)
|
||||
offloading_objects;
|
||||
explicit LocalDiskSegment(bool enable_offloading)
|
||||
: enable_offloading(enable_offloading) {}
|
||||
|
||||
LocalDiskSegment(const LocalDiskSegment&) = delete;
|
||||
LocalDiskSegment& operator=(const LocalDiskSegment&) = delete;
|
||||
|
||||
LocalDiskSegment(LocalDiskSegment&&) = delete;
|
||||
LocalDiskSegment& operator=(LocalDiskSegment&&) = delete;
|
||||
};
|
||||
|
||||
// Forward declarations
|
||||
class SegmentManager;
|
||||
|
||||
/**
|
||||
* @brief RAII-style access to segment mutex for thread-safe segment operations
|
||||
*/
|
||||
class ScopedSegmentAccess {
|
||||
public:
|
||||
/**
|
||||
* @brief Acquires a lock on the segment mutex
|
||||
* @param mutex Reference to the segment mutex
|
||||
*/
|
||||
explicit ScopedSegmentAccess(SegmentManager* segment_manager,
|
||||
std::shared_mutex& mutex)
|
||||
: segment_manager_(segment_manager), lock_(mutex) {}
|
||||
|
||||
/**
|
||||
* @brief Mount a segment
|
||||
*/
|
||||
ErrorCode MountSegment(const Segment& segment, const UUID& client_id);
|
||||
|
||||
ErrorCode MountLocalDiskSegment(const UUID& client_id,
|
||||
bool enable_offloading);
|
||||
|
||||
/**
|
||||
* @brief Re-mount a segment. To avoid infinite remount trying, only the
|
||||
* errors that may be solved by subsequent remount tryings are considered as
|
||||
* errors. When encounters unsolvable errors, the segment will not be
|
||||
* mounted while the return value will be OK.
|
||||
*/
|
||||
ErrorCode ReMountSegment(const std::vector<Segment>& segments,
|
||||
const UUID& client_id);
|
||||
|
||||
/**
|
||||
* @brief Prepare to unmount a segment by deleting its allocator
|
||||
*/
|
||||
ErrorCode PrepareUnmountSegment(const UUID& segment_id,
|
||||
size_t& metrics_dec_capacity);
|
||||
|
||||
/**
|
||||
* @brief Deleting the segment to complete the unmounting operation
|
||||
*/
|
||||
ErrorCode CommitUnmountSegment(const UUID& segment_id,
|
||||
const UUID& client_id,
|
||||
const size_t& metrics_dec_capacity);
|
||||
|
||||
/**
|
||||
* @brief Get all the segments of a client
|
||||
*/
|
||||
ErrorCode GetClientSegments(const UUID& client_id,
|
||||
std::vector<Segment>& segments) const;
|
||||
|
||||
/**
|
||||
* @brief Get the names of all the segments
|
||||
*/
|
||||
ErrorCode GetAllSegments(std::vector<std::string>& all_segments);
|
||||
|
||||
/**
|
||||
* @brief Get the segment by name. If there are multiple segments with the
|
||||
* same name, return the first one.
|
||||
*/
|
||||
ErrorCode QuerySegments(const std::string& segment, size_t& used,
|
||||
size_t& capacity);
|
||||
|
||||
private:
|
||||
SegmentManager* segment_manager_;
|
||||
std::unique_lock<std::shared_mutex> lock_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief RAII-style access to allocators for thread-safe allocator usage
|
||||
*/
|
||||
class ScopedAllocatorAccess {
|
||||
public:
|
||||
explicit ScopedAllocatorAccess(const AllocatorManager& allocator_manager,
|
||||
std::shared_mutex& mutex)
|
||||
: allocator_manager_(allocator_manager), lock_(mutex) {}
|
||||
|
||||
const AllocatorManager& getAllocatorManager() { return allocator_manager_; }
|
||||
|
||||
private:
|
||||
const AllocatorManager& allocator_manager_;
|
||||
std::shared_lock<std::shared_mutex> lock_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief RAII-style access to LocalDiskOffloadingQueues for thread-safe
|
||||
* LocalDiskOffloadingQueue usage
|
||||
*/
|
||||
class ScopedLocalDiskSegmentAccess {
|
||||
public:
|
||||
explicit ScopedLocalDiskSegmentAccess(
|
||||
std::unordered_map<std::string, UUID>& client_by_name,
|
||||
std::unordered_map<UUID, std::shared_ptr<LocalDiskSegment>,
|
||||
boost::hash<UUID>>& client_local_disk_segment,
|
||||
std::shared_mutex& mutex)
|
||||
: client_by_name_(client_by_name),
|
||||
client_local_disk_segment_(client_local_disk_segment),
|
||||
lock_(mutex) {}
|
||||
|
||||
const std::unordered_map<std::string, UUID>& getClientByName() {
|
||||
return client_by_name_;
|
||||
}
|
||||
|
||||
std::unordered_map<UUID, std::shared_ptr<LocalDiskSegment>,
|
||||
boost::hash<UUID>>&
|
||||
getClientLocalDiskSegment() {
|
||||
return client_local_disk_segment_;
|
||||
}
|
||||
|
||||
private:
|
||||
const std::unordered_map<std::string, UUID>&
|
||||
client_by_name_; // segment name -> client_id
|
||||
std::unordered_map<UUID, std::shared_ptr<LocalDiskSegment>,
|
||||
boost::hash<UUID>>& client_local_disk_segment_;
|
||||
std::shared_lock<std::shared_mutex> lock_;
|
||||
};
|
||||
|
||||
class SegmentManager {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor for SegmentManager
|
||||
* @param memory_allocator Type of buffer allocator to use for new segments
|
||||
*/
|
||||
explicit SegmentManager(
|
||||
BufferAllocatorType memory_allocator = BufferAllocatorType::CACHELIB)
|
||||
: memory_allocator_(memory_allocator) {}
|
||||
|
||||
/**
|
||||
* @brief Get RAII-style access to segment management operations
|
||||
* @return ScopedSegmentAccess object that holds the lock
|
||||
*/
|
||||
ScopedSegmentAccess getSegmentAccess() {
|
||||
return ScopedSegmentAccess(this, segment_mutex_);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get RAII-style access to use allocators
|
||||
* @return ScopedAllocatorAccess object that holds the lock
|
||||
*/
|
||||
ScopedAllocatorAccess getAllocatorAccess() {
|
||||
return ScopedAllocatorAccess(allocator_manager_, segment_mutex_);
|
||||
}
|
||||
|
||||
ScopedLocalDiskSegmentAccess getLocalDiskSegmentAccess() {
|
||||
return ScopedLocalDiskSegmentAccess(
|
||||
client_by_name_, client_local_disk_segment_, segment_mutex_);
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::shared_mutex segment_mutex_;
|
||||
std::shared_ptr<AllocationStrategy> allocation_strategy_;
|
||||
const BufferAllocatorType
|
||||
memory_allocator_; // Type of buffer allocator to use
|
||||
// allocator_manager_ only contains allocators whose segment status is OK.
|
||||
AllocatorManager allocator_manager_;
|
||||
std::unordered_map<UUID, MountedSegment, boost::hash<UUID>>
|
||||
mounted_segments_; // segment_id -> mounted segment
|
||||
std::unordered_map<UUID, std::vector<UUID>, boost::hash<UUID>>
|
||||
client_segments_; // client_id -> segment_ids
|
||||
|
||||
std::unordered_map<std::string, UUID>
|
||||
client_by_name_; // segment name -> client_id
|
||||
std::unordered_map<UUID, std::shared_ptr<LocalDiskSegment>,
|
||||
boost::hash<UUID>>
|
||||
client_local_disk_segment_; // client_id -> local_disk_segment
|
||||
|
||||
friend class ScopedSegmentAccess;
|
||||
friend class SegmentTest; // for unit tests
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mutex.h"
|
||||
#include "types.h"
|
||||
#include "ylt/util/tl/expected.hpp"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class SegmentManager {
|
||||
public:
|
||||
virtual ~SegmentManager() = default;
|
||||
|
||||
auto MountSegment(const Segment& segment) -> tl::expected<void, ErrorCode>;
|
||||
auto UnmountSegment(const UUID& segment_id)
|
||||
-> tl::expected<void, ErrorCode>;
|
||||
// TODO: wanyue-wy
|
||||
// There is currently no mechanism to guarantee `segment_name`'s uniqueness
|
||||
// within the cluster.
|
||||
// For backward compatibility during refactoring, we temporarily maintain a
|
||||
// weak assumption of `segment_name`'s uniqueness.
|
||||
// However, before merging to main, we need to discuss whether name
|
||||
// uniqueness is necessary and whether the query interface definition should
|
||||
// be modified.
|
||||
virtual auto QuerySegments(const std::string& segment)
|
||||
-> tl::expected<std::pair<size_t, size_t>, ErrorCode> = 0;
|
||||
auto QuerySegment(const UUID& segment_id)
|
||||
-> tl::expected<std::shared_ptr<Segment>, ErrorCode>;
|
||||
auto GetSegments() -> tl::expected<std::vector<Segment>, ErrorCode>;
|
||||
|
||||
using SegmentRemovalCallback = std::function<void(const UUID& segment_id)>;
|
||||
void SetSegmentRemovalCallback(SegmentRemovalCallback cb);
|
||||
|
||||
protected:
|
||||
// Pure virtual inner methods (Implementation specific, No locking required)
|
||||
virtual auto InnerMountSegment(const Segment& segment)
|
||||
-> tl::expected<void, ErrorCode> = 0;
|
||||
|
||||
virtual auto OnUnmountSegment(const std::shared_ptr<Segment>& segment)
|
||||
-> tl::expected<void, ErrorCode> = 0;
|
||||
|
||||
protected:
|
||||
mutable SharedMutex segment_mutex_;
|
||||
SegmentRemovalCallback segment_removal_cb_;
|
||||
|
||||
std::unordered_map<UUID, std::shared_ptr<Segment>, boost::hash<UUID>>
|
||||
mounted_segments_
|
||||
GUARDED_BY(segment_mutex_); // segment_id -> mounted segment
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -2,9 +2,14 @@
|
|||
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "file_interface.h"
|
||||
|
|
@ -33,6 +38,8 @@ struct BucketMetadata {
|
|||
YLT_REFL(BucketMetadata, data_size, keys, metadatas);
|
||||
|
||||
struct OffloadMetadata {
|
||||
// `total_keys` tracks live keys reachable through the backend.
|
||||
// `total_size` tracks backend-accounted used bytes.
|
||||
int64_t total_keys;
|
||||
int64_t total_size;
|
||||
OffloadMetadata(std::size_t keys, int64_t size)
|
||||
|
|
@ -131,9 +138,62 @@ class StorageBackendInterface {
|
|||
const std::vector<std::string>& keys,
|
||||
std::vector<StorageObjectMetadata>& metadatas)>& handler) = 0;
|
||||
|
||||
/**
|
||||
* @brief Mark a key as deleted.
|
||||
* @param key The object key that was deleted
|
||||
* @return tl::expected<void, ErrorCode> indicating operation status
|
||||
* @note FilePerKey physically removes the file and updates physical
|
||||
* accounting. Bucket backend removes the live mapping but defers physical
|
||||
* reclaim to bucket eviction.
|
||||
*/
|
||||
virtual tl::expected<void, ErrorCode> MarkKeyDeleted(
|
||||
const std::string& key) {
|
||||
// Default implementation: no-op
|
||||
return {};
|
||||
}
|
||||
|
||||
FileStorageConfig file_storage_config_;
|
||||
};
|
||||
|
||||
class LocalStorageSpaceManager {
|
||||
public:
|
||||
explicit LocalStorageSpaceManager(
|
||||
std::filesystem::path storage_root = std::filesystem::path());
|
||||
|
||||
void SetStorageRoot(std::filesystem::path storage_root);
|
||||
|
||||
tl::expected<void, ErrorCode> Init(uint64_t used_space_bytes,
|
||||
uint64_t quota_bytes = 0);
|
||||
|
||||
tl::expected<bool, ErrorCode> HasPhysicalSpace(
|
||||
uint64_t required_size) const;
|
||||
|
||||
bool IsInitialized() const;
|
||||
|
||||
bool TryReserve(uint64_t required_size);
|
||||
|
||||
void Release(uint64_t size_to_release);
|
||||
|
||||
uint64_t TotalSpace() const;
|
||||
|
||||
uint64_t UsedSpace() const;
|
||||
|
||||
uint64_t AvailableSpace() const;
|
||||
|
||||
bool IsOverQuota() const;
|
||||
|
||||
private:
|
||||
void RecalculateAvailableSpaceLocked();
|
||||
|
||||
private:
|
||||
std::filesystem::path storage_root_;
|
||||
mutable std::shared_mutex mutex_;
|
||||
uint64_t total_space_ = 0;
|
||||
uint64_t used_space_ = 0;
|
||||
uint64_t available_space_ = 0;
|
||||
std::atomic<bool> initialized_{false};
|
||||
};
|
||||
|
||||
/**
|
||||
* @class StorageBackend
|
||||
* @brief Implementation of StorageBackend interface using local filesystem
|
||||
|
|
@ -295,7 +355,7 @@ class StorageBackend {
|
|||
* @brief Deletes the physical file associated with the given object key
|
||||
* @param path Path to the file to remove
|
||||
*/
|
||||
void RemoveFile(const std::string& path);
|
||||
tl::expected<void, ErrorCode> RemoveFile(const std::string& path);
|
||||
|
||||
/**
|
||||
* @brief Removes objects from the storage backend whose keys match a regex
|
||||
|
|
@ -334,14 +394,8 @@ class StorageBackend {
|
|||
mutable std::shared_mutex
|
||||
file_queue_mutex_; // Mutex to protect file queue operations
|
||||
|
||||
// Storage space tracking variables
|
||||
mutable std::shared_mutex
|
||||
space_mutex_; // Mutex to protect space tracking variables
|
||||
uint64_t total_space_ = 0; // Total storage space in bytes
|
||||
uint64_t used_space_ = 0; // Used storage space in bytes
|
||||
uint64_t available_space_ = 0; // Available storage space in bytes
|
||||
|
||||
std::atomic<bool> initialized_{false};
|
||||
LocalStorageSpaceManager space_manager_;
|
||||
|
||||
/**
|
||||
* @brief Make sure the path is valid and create necessary directories
|
||||
|
|
@ -409,12 +463,6 @@ class StorageBackend {
|
|||
*/
|
||||
void ReleaseSpace(uint64_t size_to_release);
|
||||
|
||||
/**
|
||||
* @brief Recalculates available_space_ based on total_space_ and
|
||||
* used_space_. Must be called with space_mutex_ locked.
|
||||
*/
|
||||
void RecalculateAvailableSpace();
|
||||
|
||||
/**
|
||||
* @brief Gets the actual filesystem directory name by removing "moon_"
|
||||
* prefix if present.
|
||||
|
|
@ -506,6 +554,9 @@ class StorageBackendAdaptor : public StorageBackendInterface {
|
|||
const std::vector<std::string>& keys,
|
||||
std::vector<StorageObjectMetadata>& metadatas)>& handler) override;
|
||||
|
||||
tl::expected<void, ErrorCode> MarkKeyDeleted(
|
||||
const std::string& key) override;
|
||||
|
||||
private:
|
||||
const FilePerKeyConfig file_per_key_config_;
|
||||
|
||||
|
|
@ -519,6 +570,7 @@ class StorageBackendAdaptor : public StorageBackendInterface {
|
|||
|
||||
static std::string ConcatSlicesToString(const std::vector<Slice>& slices);
|
||||
|
||||
mutable Mutex scan_mutex_;
|
||||
mutable Mutex mutex_;
|
||||
|
||||
int64_t total_keys GUARDED_BY(mutex_);
|
||||
|
|
@ -542,6 +594,7 @@ class BucketStorageBackend : public StorageBackendInterface {
|
|||
public:
|
||||
BucketStorageBackend(const FileStorageConfig& file_storage_config_,
|
||||
const BucketBackendConfig& bucket_backend_config_);
|
||||
~BucketStorageBackend();
|
||||
|
||||
/**
|
||||
* @brief Offload objects in batches
|
||||
|
|
@ -658,11 +711,36 @@ class BucketStorageBackend : public StorageBackendInterface {
|
|||
|
||||
/**
|
||||
* @brief Retrieves the global metadata of the store.
|
||||
* @return On success: `tl::expected` containing a `StoreMetadata`
|
||||
* object. On failure: an error code.
|
||||
* @return On success: `tl::expected` containing live key count and
|
||||
* backend-accounted used bytes. On failure: an error code.
|
||||
*/
|
||||
tl::expected<OffloadMetadata, ErrorCode> GetStoreMetadata();
|
||||
|
||||
/**
|
||||
* @brief Select a bucket for eviction based on fragmentation and age.
|
||||
* @return tl::expected<int64_t, ErrorCode>
|
||||
* - On success: the bucket ID to evict
|
||||
* - On failure: error code (e.g., OBJECT_NOT_FOUND if no buckets exist)
|
||||
*/
|
||||
tl::expected<int64_t, ErrorCode> SelectBucketForEviction() const;
|
||||
|
||||
/**
|
||||
* @brief Evict an entire bucket by removing its data and metadata files.
|
||||
* @param bucket_id The ID of the bucket to evict
|
||||
* @return tl::expected<size_t, ErrorCode>
|
||||
* - On success: the amount of space freed (in bytes)
|
||||
* - On failure: error code
|
||||
*/
|
||||
tl::expected<size_t, ErrorCode> EvictBucket(int64_t bucket_id);
|
||||
|
||||
/**
|
||||
* @brief Mark a key as deleted (for fragmentation tracking).
|
||||
* @param key The object key that was deleted
|
||||
* @return tl::expected<void, ErrorCode> indicating operation status
|
||||
*/
|
||||
tl::expected<void, ErrorCode> MarkKeyDeleted(
|
||||
const std::string& key) override;
|
||||
|
||||
private:
|
||||
tl::expected<std::shared_ptr<BucketMetadata>, ErrorCode> BuildBucket(
|
||||
int64_t bucket_id,
|
||||
|
|
@ -675,7 +753,11 @@ class BucketStorageBackend : public StorageBackendInterface {
|
|||
std::vector<iovec>& iovs);
|
||||
|
||||
tl::expected<void, ErrorCode> StoreBucketMetadata(
|
||||
int64_t bucket_id, std::shared_ptr<BucketMetadata> bucket_metadata);
|
||||
const std::string& metadata_path,
|
||||
const std::string& serialized_metadata);
|
||||
|
||||
tl::expected<std::string, ErrorCode> SerializeBucketMetadata(
|
||||
const std::shared_ptr<BucketMetadata>& bucket_metadata);
|
||||
|
||||
tl::expected<void, ErrorCode> LoadBucketMetadata(
|
||||
int64_t bucket_id, std::shared_ptr<BucketMetadata> bucket_metadata);
|
||||
|
|
@ -706,6 +788,27 @@ class BucketStorageBackend : public StorageBackendInterface {
|
|||
|
||||
tl::expected<bool, ErrorCode> HasNext();
|
||||
|
||||
struct PendingBucketDeletion {
|
||||
int64_t bucket_id;
|
||||
uint64_t data_bytes;
|
||||
uint64_t meta_bytes;
|
||||
uint64_t queued_bytes;
|
||||
std::string data_path;
|
||||
std::string meta_path;
|
||||
};
|
||||
|
||||
tl::expected<bool, ErrorCode> CanAcceptAnotherBucket() const;
|
||||
|
||||
uint64_t MaxPendingDeletionBytes() const;
|
||||
|
||||
void StartDeletionWorker();
|
||||
|
||||
void StopDeletionWorker();
|
||||
|
||||
void EnqueueBucketDeletion(PendingBucketDeletion task);
|
||||
|
||||
void BucketDeletionWorker();
|
||||
|
||||
private:
|
||||
std::atomic<bool> initialized_{false};
|
||||
std::optional<BucketIdGenerator> bucket_id_generator_;
|
||||
|
|
@ -718,25 +821,37 @@ class BucketStorageBackend : public StorageBackendInterface {
|
|||
* metadata members:
|
||||
* - object_bucket_map_: maps object keys to bucket IDs
|
||||
* - buckets_: ordered map of bucket ID to bucket metadata
|
||||
* - total_size_: cumulative data size of all stored objects
|
||||
* - physical_used_bytes_: physical bytes still present on local storage
|
||||
*/
|
||||
mutable SharedMutex mutex_;
|
||||
mutable Mutex iterator_mutex_;
|
||||
std::string storage_path_;
|
||||
int64_t total_size_ GUARDED_BY(mutex_) = 0;
|
||||
int64_t physical_used_bytes_ GUARDED_BY(mutex_) = 0;
|
||||
std::unordered_map<std::string, StorageObjectMetadata> GUARDED_BY(mutex_)
|
||||
object_bucket_map_;
|
||||
std::map<int64_t, std::shared_ptr<BucketMetadata>> GUARDED_BY(
|
||||
mutex_) buckets_;
|
||||
int64_t GUARDED_BY(mutex_) next_bucket_ = -1;
|
||||
BucketBackendConfig bucket_backend_config_;
|
||||
LocalStorageSpaceManager space_manager_;
|
||||
|
||||
mutable Mutex offloading_mutex_;
|
||||
std::unordered_map<std::string, int64_t> GUARDED_BY(offloading_mutex_)
|
||||
ungrouped_offloading_objects_;
|
||||
|
||||
// Track valid key count per bucket for fragmentation calculation
|
||||
std::unordered_map<int64_t, int> GUARDED_BY(mutex_) bucket_valid_keys_;
|
||||
|
||||
std::mutex deletion_mutex_;
|
||||
std::condition_variable deletion_cv_;
|
||||
std::deque<PendingBucketDeletion> pending_bucket_deletions_;
|
||||
std::thread deletion_thread_;
|
||||
bool stop_deletion_worker_ = false;
|
||||
std::atomic<uint64_t> pending_deletion_bytes_{0};
|
||||
std::atomic<uint64_t> pending_deletion_count_{0};
|
||||
};
|
||||
|
||||
tl::expected<std::shared_ptr<StorageBackendInterface>, ErrorCode>
|
||||
CreateStorageBackend(const FileStorageConfig& config);
|
||||
|
||||
} // namespace mooncake
|
||||
} // namespace mooncake
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
#pragma once
|
||||
|
||||
#include <future>
|
||||
#include <memory>
|
||||
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
// ============================================================================
|
||||
// TaskHandle<V> — abstract base for pending operations.
|
||||
// Wait() returns tl::expected<V, ErrorCode>, where ErrorCode is the error type
|
||||
// and V is the value type on success.
|
||||
// ============================================================================
|
||||
|
||||
template <typename V>
|
||||
class TaskHandle {
|
||||
public:
|
||||
virtual ~TaskHandle() = default;
|
||||
virtual tl::expected<V, ErrorCode> Wait() = 0;
|
||||
};
|
||||
|
||||
template <typename V>
|
||||
class ImmediateHandle : public TaskHandle<V> {
|
||||
public:
|
||||
tl::expected<V, ErrorCode> Wait() override { return {}; }
|
||||
|
||||
static std::unique_ptr<ImmediateHandle<V>> Create() {
|
||||
return std::make_unique<ImmediateHandle<V>>();
|
||||
}
|
||||
};
|
||||
|
||||
template <typename V>
|
||||
class CallableTaskHandle : public TaskHandle<V> {
|
||||
public:
|
||||
template <typename F>
|
||||
explicit CallableTaskHandle(F&& fn)
|
||||
: impl_(
|
||||
std::make_unique<Wrapper<std::decay_t<F>>>(std::forward<F>(fn))) {
|
||||
}
|
||||
|
||||
tl::expected<V, ErrorCode> Wait() override { return impl_->invoke(); }
|
||||
|
||||
template <typename F>
|
||||
static std::unique_ptr<CallableTaskHandle<V>> Create(F&& fn) {
|
||||
return std::make_unique<CallableTaskHandle<V>>(std::forward<F>(fn));
|
||||
}
|
||||
|
||||
private:
|
||||
struct Impl {
|
||||
virtual ~Impl() = default;
|
||||
virtual tl::expected<V, ErrorCode> invoke() = 0;
|
||||
};
|
||||
|
||||
template <typename F>
|
||||
struct Wrapper final : Impl {
|
||||
explicit Wrapper(F&& f) : fn(std::move(f)) {}
|
||||
tl::expected<V, ErrorCode> invoke() override { return fn(); }
|
||||
|
||||
F fn;
|
||||
};
|
||||
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
template <typename V>
|
||||
class RemoteRpcHandle : public TaskHandle<V> {
|
||||
public:
|
||||
RemoteRpcHandle(std::shared_ptr<void> request_storage,
|
||||
std::future<tl::expected<V, ErrorCode>> future)
|
||||
: request_storage_(std::move(request_storage)),
|
||||
future_(std::move(future)) {}
|
||||
|
||||
tl::expected<V, ErrorCode> Wait() override { return future_.get(); }
|
||||
|
||||
template <typename T>
|
||||
static std::unique_ptr<RemoteRpcHandle<V>> Create(
|
||||
std::shared_ptr<T> request_storage,
|
||||
std::future<tl::expected<V, ErrorCode>> future) {
|
||||
return std::make_unique<RemoteRpcHandle<V>>(std::move(request_storage),
|
||||
std::move(future));
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<void> request_storage_;
|
||||
std::future<tl::expected<V, ErrorCode>> future_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
// ThreadPool.h
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
#pragma once
|
||||
|
||||
#include "tiered_cache/tiers/cache_tier.h"
|
||||
#include "tiered_cache/data_copier.h"
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
// Forward declaration from data_copier.h to avoid circular dependency
|
||||
class DataCopierBuilder;
|
||||
|
||||
// Holds the registration information for a memory type.
|
||||
struct MemoryTypeRegistration {
|
||||
MemoryType type;
|
||||
CopyFunction to_dram_func;
|
||||
CopyFunction from_dram_func;
|
||||
};
|
||||
|
||||
// Holds the registration for an optimized direct path.
|
||||
struct DirectPathRegistration {
|
||||
MemoryType src_type;
|
||||
MemoryType dest_type;
|
||||
CopyFunction func;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A singleton registry for data copier functions.
|
||||
*
|
||||
* Modules can register their copy functions here during static initialization.
|
||||
* The DataCopierBuilder will then use this registry to construct a DataCopier.
|
||||
*/
|
||||
class CopierRegistry {
|
||||
public:
|
||||
/**
|
||||
* @brief Get the singleton instance of the registry.
|
||||
*/
|
||||
static CopierRegistry& GetInstance();
|
||||
|
||||
/**
|
||||
* @brief Registers the to/from DRAM copy functions for a memory type.
|
||||
*/
|
||||
void RegisterMemoryType(MemoryType type, CopyFunction to_dram,
|
||||
CopyFunction from_dram);
|
||||
|
||||
/**
|
||||
* @brief Registers an optional, optimized direct copy path.
|
||||
*/
|
||||
void RegisterDirectPath(MemoryType src, MemoryType dest, CopyFunction func);
|
||||
|
||||
// These methods are used by the DataCopierBuilder to collect all
|
||||
// registrations.
|
||||
const std::vector<MemoryTypeRegistration>& GetMemoryTypeRegistrations()
|
||||
const;
|
||||
const std::vector<DirectPathRegistration>& GetDirectPathRegistrations()
|
||||
const;
|
||||
|
||||
private:
|
||||
friend class DataCopierBuilder;
|
||||
|
||||
CopierRegistry() = default;
|
||||
~CopierRegistry() = default;
|
||||
CopierRegistry(const CopierRegistry&) = delete;
|
||||
CopierRegistry& operator=(const CopierRegistry&) = delete;
|
||||
|
||||
std::vector<MemoryTypeRegistration> memory_type_regs_;
|
||||
std::vector<DirectPathRegistration> direct_path_regs_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A helper class to automatically register copiers at static
|
||||
* initialization time.
|
||||
*
|
||||
* To register a new memory type, simply declare a static instance of this class
|
||||
* in the corresponding .cpp file, providing the type and its to/from DRAM
|
||||
* copiers.
|
||||
*/
|
||||
class CopierRegistrar {
|
||||
public:
|
||||
CopierRegistrar(MemoryType type, CopyFunction to_dram,
|
||||
CopyFunction from_dram);
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <glog/logging.h>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include "rpc_types.h"
|
||||
#include "tiered_cache/tiers/cache_tier.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
using CopyFunction = std::function<tl::expected<void, ErrorCode>(
|
||||
const DataSource& src, const DataSource& dst)>;
|
||||
|
||||
class DataCopier;
|
||||
|
||||
/**
|
||||
* @brief A helper class to build a valid DataCopier.
|
||||
*
|
||||
* This builder enforces the rule that for any new memory type added,
|
||||
* its copy functions to and from DRAM *must* be provided via the
|
||||
* CopierRegistry.
|
||||
*/
|
||||
class DataCopierBuilder {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a builder. It automatically pulls all existing
|
||||
* registrations from the global CopierRegistry.
|
||||
*/
|
||||
DataCopierBuilder();
|
||||
|
||||
/**
|
||||
* @brief (Optional) Registers a highly optimized direct copy path.
|
||||
* This will be used instead of the DRAM fallback. Can be used for testing
|
||||
* or for paths that are not self-registered.
|
||||
* @return A reference to the builder for chaining.
|
||||
*/
|
||||
DataCopierBuilder& AddDirectPath(MemoryType src_type, MemoryType dest_type,
|
||||
CopyFunction func);
|
||||
|
||||
/**
|
||||
* @brief Builds the final, immutable DataCopier object.
|
||||
* It verifies that all memory types defined in the MemoryType enum
|
||||
* have been registered via the registry before creating the object.
|
||||
* @return A unique_ptr to the new DataCopier.
|
||||
* @throws std::logic_error if a required to/from DRAM copier is missing.
|
||||
*/
|
||||
std::unique_ptr<DataCopier> Build() const;
|
||||
|
||||
private:
|
||||
std::map<std::pair<MemoryType, MemoryType>, CopyFunction> copy_matrix_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A central utility for copying data between different memory types.
|
||||
* It supports a fallback mechanism via DRAM for any copy paths that are not
|
||||
* explicitly registered as a direct path.
|
||||
*/
|
||||
class DataCopier {
|
||||
public:
|
||||
// The constructor is private. Use DataCopierBuilder to create an instance.
|
||||
~DataCopier() = default;
|
||||
DataCopier(const DataCopier&) = delete;
|
||||
DataCopier& operator=(const DataCopier&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Executes a copy from a source to a destination.
|
||||
* It first attempts to find a direct copy function (e.g., VRAM -> VRAM).
|
||||
* If not found, it automatically falls back to a two-step copy via a
|
||||
* temporary DRAM buffer (e.g., VRAM -> DRAM -> SSD).
|
||||
* @param src The data source descriptor.
|
||||
* @param dest_type The memory type of the destination.
|
||||
* @param dest_ptr A pointer to the destination (memory address, handle,
|
||||
* etc.).
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Copy(const DataSource& src,
|
||||
const DataSource& dst) const;
|
||||
|
||||
private:
|
||||
friend class DataCopierBuilder; // Allow builder to access the constructor.
|
||||
DataCopier(
|
||||
std::map<std::pair<MemoryType, MemoryType>, CopyFunction> copy_matrix);
|
||||
|
||||
CopyFunction FindCopier(MemoryType src_type, MemoryType dest_type) const;
|
||||
const std::map<std::pair<MemoryType, MemoryType>, CopyFunction>
|
||||
copy_matrix_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <string_view>
|
||||
#include "mutex.h"
|
||||
#include "tiered_cache/scheduler/scheduler_policy.h"
|
||||
#include "tiered_cache/scheduler/stats_collector.h"
|
||||
#include "types.h"
|
||||
|
||||
#include <json/value.h>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class TieredBackend; // Forward declaration
|
||||
class CacheTier;
|
||||
|
||||
/**
|
||||
* @class ClientScheduler
|
||||
* @brief Coordinates statistics collection, policy execution, and action
|
||||
* application.
|
||||
*/
|
||||
class ClientScheduler {
|
||||
public:
|
||||
ClientScheduler(TieredBackend* backend, const Json::Value& config);
|
||||
~ClientScheduler();
|
||||
|
||||
// Lifecycle management
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
// Register a managed tier
|
||||
void RegisterTier(CacheTier* tier);
|
||||
|
||||
// Incoming event hook (thread-safe)
|
||||
void OnAccess(const std::string& key);
|
||||
|
||||
/**
|
||||
* @brief Get current hot key statistics for HA recovery prioritization.
|
||||
*/
|
||||
AccessStats GetHotKeyStats() const;
|
||||
|
||||
// Called when a replica is committed or updated
|
||||
void OnCommit(const std::string& key, UUID tier_id, size_t size_bytes);
|
||||
|
||||
// Called when a key or a replica is deleted
|
||||
void OnDelete(const std::string& key,
|
||||
std::optional<UUID> tier_id = std::nullopt);
|
||||
|
||||
// Called when allocation fails due to insufficient space
|
||||
// Returns true if reclaim freed enough space for an immediate retry
|
||||
bool OnAllocationFailure(UUID tier_id, size_t required_bytes);
|
||||
|
||||
private:
|
||||
struct PlannedReclaim {
|
||||
struct Step {
|
||||
SchedAction action;
|
||||
size_t size_bytes = 0;
|
||||
};
|
||||
|
||||
std::vector<Step> steps;
|
||||
size_t target_reclaim_bytes = 0;
|
||||
};
|
||||
|
||||
// Background worker loop
|
||||
void WorkerLoop();
|
||||
|
||||
// Execute generated actions
|
||||
void ExecuteActions(const std::vector<SchedAction>& actions);
|
||||
|
||||
// Trigger immediate eviction for a tier (sync mode)
|
||||
bool TriggerSyncEviction(UUID tier_id, size_t required_bytes);
|
||||
|
||||
// Reclaim pre-replicated cold replicas without copying data on the
|
||||
// allocation failure path.
|
||||
bool TryFastReclaim(UUID tier_id, size_t required_bytes);
|
||||
|
||||
PlannedReclaim BuildReclaimPlan(
|
||||
UUID tier_id, const std::unordered_map<UUID, TierStats>& tier_stats,
|
||||
const std::vector<KeyContext>& active_keys,
|
||||
bool require_existing_replica, size_t required_bytes) const;
|
||||
|
||||
size_t ExecuteReclaimPlan(const PlannedReclaim& plan);
|
||||
bool HasAvailableBytes(UUID tier_id, size_t required_bytes) const;
|
||||
std::optional<UUID> SelectDemotionTier(UUID source_tier_id) const;
|
||||
|
||||
// Build policy input from the latest stats snapshot and scheduler cache
|
||||
std::vector<KeyContext> BuildActiveKeys(
|
||||
const AccessStats& access_stats,
|
||||
std::optional<UUID> pinned_tier_id = std::nullopt);
|
||||
|
||||
// Build a fresh tier stats map for policy execution
|
||||
std::unordered_map<UUID, TierStats> CollectTierStats() const;
|
||||
|
||||
struct CachedKeyState {
|
||||
size_t size_bytes = 0;
|
||||
std::vector<UUID> current_locations;
|
||||
};
|
||||
|
||||
struct KeyCacheShard {
|
||||
mutable Mutex mutex;
|
||||
std::unordered_map<std::string, CachedKeyState> key_cache
|
||||
GUARDED_BY(mutex);
|
||||
std::unordered_map<UUID, std::unordered_set<std::string>>
|
||||
tier_resident_keys GUARDED_BY(mutex);
|
||||
};
|
||||
|
||||
static constexpr size_t kKeyCacheShardCount = 16;
|
||||
|
||||
static size_t KeyCacheShardIndex(std::string_view key);
|
||||
KeyCacheShard& GetKeyCacheShard(std::string_view key);
|
||||
const KeyCacheShard& GetKeyCacheShard(std::string_view key) const;
|
||||
|
||||
size_t EstimateActiveKeyReserve(const AccessStats& access_stats,
|
||||
std::optional<UUID> pinned_tier_id) const;
|
||||
void AppendHotKeys(const AccessStats& access_stats,
|
||||
std::vector<KeyContext>& active_keys,
|
||||
std::unordered_set<std::string>& seen_keys) const;
|
||||
void AppendPinnedTierKeys(UUID pinned_tier_id,
|
||||
std::vector<KeyContext>& active_keys,
|
||||
std::unordered_set<std::string>& seen_keys) const;
|
||||
std::optional<KeyContext> BuildKeyContextLocked(
|
||||
const std::string& key, const CachedKeyState& state,
|
||||
const AccessStats& access_stats,
|
||||
const AccessStatEntry* stat_entry = nullptr) const;
|
||||
size_t GetCachedKeySize(const std::string& key) const;
|
||||
|
||||
void TrackReplicaLocked(KeyCacheShard& shard, const std::string& key,
|
||||
UUID tier_id, size_t size_bytes)
|
||||
REQUIRES(shard.mutex);
|
||||
bool RemoveReplicaLocked(KeyCacheShard& shard, const std::string& key,
|
||||
std::optional<UUID> tier_id) REQUIRES(shard.mutex);
|
||||
|
||||
private:
|
||||
TieredBackend* backend_;
|
||||
std::unique_ptr<SchedulerPolicy> policy_;
|
||||
std::unique_ptr<StatsCollector> stats_collector_;
|
||||
|
||||
std::atomic<bool> running_{false};
|
||||
std::thread worker_thread_;
|
||||
|
||||
// Local view of tiers for policy input
|
||||
std::unordered_map<UUID, CacheTier*> tiers_;
|
||||
|
||||
// Scheduler-side metadata cache to avoid full backend scans each cycle
|
||||
std::array<KeyCacheShard, kKeyCacheShardCount> key_cache_shards_;
|
||||
std::optional<UUID> fast_tier_id_;
|
||||
|
||||
// Configuration
|
||||
int loop_interval_ms_ = 1000;
|
||||
size_t stats_snapshot_limit_ = detail::DefaultSnapshotLimit();
|
||||
enum class EvictionMode { SYNC, ASYNC };
|
||||
EvictionMode eviction_mode_ = EvictionMode::ASYNC;
|
||||
|
||||
// Used to wake the worker thread immediately on Stop().
|
||||
std::mutex cv_mutex_;
|
||||
std::condition_variable cv_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include "tiered_cache/scheduler/scheduler_policy.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @class LRUPolicy
|
||||
* @brief Recency-based promotion/eviction policy with watermark control.
|
||||
*
|
||||
* All keys are sorted by recency. The most recently used keys (up to target
|
||||
* capacity) should be in fast tier, the rest in slow tier.
|
||||
*/
|
||||
class LRUPolicy : public SchedulerPolicy {
|
||||
public:
|
||||
struct Config {
|
||||
double high_watermark = 0.90; // Trigger scheduling when above this
|
||||
double low_watermark = 0.70; // Target usage after scheduling
|
||||
};
|
||||
|
||||
explicit LRUPolicy(Config config);
|
||||
|
||||
void SetFastTier(UUID id) override;
|
||||
|
||||
bool IsFastTier(UUID id) const;
|
||||
|
||||
tl::expected<std::vector<SchedAction>, ErrorCode> Decide(
|
||||
const std::unordered_map<UUID, TierStats>& tier_stats,
|
||||
const std::vector<KeyContext>& active_keys) override;
|
||||
|
||||
private:
|
||||
Config config_;
|
||||
std::optional<UUID> fast_tier_id_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "tiered_cache/scheduler/stats_collector.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @class LRUStatsCollector
|
||||
* @brief Maintains an LRU list of accessed keys.
|
||||
* Used for LRU-based promotion and eviction policies.
|
||||
*/
|
||||
class LRUStatsCollector : public StatsCollector {
|
||||
public:
|
||||
explicit LRUStatsCollector(
|
||||
size_t shard_count = detail::DefaultStatsShardCount(),
|
||||
size_t max_snapshot_keys = detail::DefaultSnapshotLimit())
|
||||
: shards_(detail::NormalizeShardCount(shard_count)),
|
||||
shard_mask_(shards_.size() - 1),
|
||||
max_snapshot_keys_(
|
||||
detail::NormalizeSnapshotLimit(max_snapshot_keys)) {}
|
||||
|
||||
void RecordAccess(const std::string& key) override {
|
||||
const uint64_t sequence =
|
||||
next_sequence_.fetch_add(1, std::memory_order_relaxed);
|
||||
auto& shard = GetShard(key);
|
||||
std::lock_guard<std::mutex> lock(shard.mutex);
|
||||
shard.pending_updates[key] = sequence;
|
||||
}
|
||||
|
||||
AccessStats GetSnapshot() override {
|
||||
std::lock_guard<std::mutex> snapshot_lock(snapshot_mutex_);
|
||||
ApplyPendingChanges();
|
||||
return BuildSnapshot();
|
||||
}
|
||||
|
||||
void RemoveKey(const std::string& key) override {
|
||||
auto& shard = GetShard(key);
|
||||
std::lock_guard<std::mutex> lock(shard.mutex);
|
||||
shard.pending_updates.erase(key);
|
||||
shard.pending_deletes.push_back(key);
|
||||
}
|
||||
|
||||
private:
|
||||
struct OrderedKey {
|
||||
uint64_t sequence;
|
||||
std::string key;
|
||||
};
|
||||
|
||||
struct OrderedKeyCompare {
|
||||
bool operator()(const OrderedKey& lhs, const OrderedKey& rhs) const {
|
||||
if (lhs.sequence != rhs.sequence) {
|
||||
return lhs.sequence > rhs.sequence;
|
||||
}
|
||||
return lhs.key < rhs.key;
|
||||
}
|
||||
};
|
||||
|
||||
struct alignas(64) Shard {
|
||||
std::mutex mutex;
|
||||
std::unordered_map<std::string, uint64_t> pending_updates;
|
||||
std::vector<std::string> pending_deletes;
|
||||
};
|
||||
|
||||
Shard& GetShard(const std::string& key) {
|
||||
const auto shard_index = std::hash<std::string>{}(key)&shard_mask_;
|
||||
return shards_[shard_index];
|
||||
}
|
||||
|
||||
void ApplyPendingChanges() {
|
||||
for (auto& shard : shards_) {
|
||||
std::unordered_map<std::string, uint64_t> pending_updates;
|
||||
std::vector<std::string> pending_deletes;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shard.mutex);
|
||||
pending_updates.swap(shard.pending_updates);
|
||||
pending_deletes.swap(shard.pending_deletes);
|
||||
}
|
||||
|
||||
for (const auto& key : pending_deletes) {
|
||||
RemoveTrackedKey(key);
|
||||
}
|
||||
|
||||
for (const auto& [key, sequence] : pending_updates) {
|
||||
auto it = latest_access_.find(key);
|
||||
if (it != latest_access_.end()) {
|
||||
ordered_keys_.erase(OrderedKey{it->second, key});
|
||||
it->second = sequence;
|
||||
} else {
|
||||
latest_access_[key] = sequence;
|
||||
}
|
||||
ordered_keys_.insert(OrderedKey{sequence, key});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccessStats BuildSnapshot() const {
|
||||
AccessStats stats;
|
||||
stats.metric = AccessStatMetric::kRecencyRank;
|
||||
stats.hot_keys.reserve(
|
||||
std::min(max_snapshot_keys_, ordered_keys_.size()));
|
||||
|
||||
size_t recency_rank = 1;
|
||||
size_t emitted = 0;
|
||||
for (const auto& ordered_key : ordered_keys_) {
|
||||
stats.hot_keys.push_back(
|
||||
AccessStatEntry{ordered_key.key, 0.0, recency_rank++});
|
||||
emitted++;
|
||||
if (emitted >= max_snapshot_keys_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
void RemoveTrackedKey(const std::string& key) {
|
||||
auto it = latest_access_.find(key);
|
||||
if (it == latest_access_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ordered_keys_.erase(OrderedKey{it->second, key});
|
||||
latest_access_.erase(it);
|
||||
}
|
||||
|
||||
std::atomic<uint64_t> next_sequence_{1};
|
||||
std::mutex snapshot_mutex_;
|
||||
std::vector<Shard> shards_;
|
||||
size_t shard_mask_;
|
||||
size_t max_snapshot_keys_;
|
||||
std::unordered_map<std::string, uint64_t> latest_access_;
|
||||
std::set<OrderedKey, OrderedKeyCompare> ordered_keys_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @struct TierStats
|
||||
* @brief Runtime statistics for a specific storage tier
|
||||
*/
|
||||
struct TierStats {
|
||||
size_t total_capacity_bytes = 0;
|
||||
size_t used_capacity_bytes = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct KeyContext
|
||||
* @brief Context for a key, including access metadata and current location.
|
||||
*/
|
||||
struct KeyContext {
|
||||
std::string key;
|
||||
double recent_heat_score = 0.0;
|
||||
size_t recency_rank = 0;
|
||||
std::vector<UUID> current_locations; // Which tiers currently hold this key
|
||||
size_t size_bytes = 0; // Size of the key's data in bytes
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct SchedAction
|
||||
* @brief A single scheduling action decision
|
||||
*/
|
||||
struct SchedAction {
|
||||
enum class Type {
|
||||
REPLICATE, // Copy data from Source to Target and keep the source copy
|
||||
MIGRATE, // Move data from Source to Target (Promote/Offload)
|
||||
EVICT, // Delete data from Source
|
||||
};
|
||||
|
||||
Type type;
|
||||
std::string key;
|
||||
|
||||
// For MIGRATE: Source and Target must be specified
|
||||
// For EVICT: Only Source is required
|
||||
std::optional<UUID> source_tier_id;
|
||||
std::optional<UUID> target_tier_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class SchedulerPolicy
|
||||
* @brief Abstract interface for scheduling algorithms
|
||||
*/
|
||||
class SchedulerPolicy {
|
||||
public:
|
||||
virtual ~SchedulerPolicy() = default;
|
||||
|
||||
/**
|
||||
* @brief Core decision function
|
||||
* @param tier_stats Current status of all managed tiers
|
||||
* @param active_keys List of active/hot keys with their context
|
||||
* @return List of recommended actions or an error when the policy cannot
|
||||
* produce a valid plan
|
||||
*/
|
||||
virtual tl::expected<std::vector<SchedAction>, ErrorCode> Decide(
|
||||
const std::unordered_map<UUID, TierStats>& tier_stats,
|
||||
const std::vector<KeyContext>& active_keys) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set the fast tier ID for the policy.
|
||||
* @param id The UUID of the fast tier (e.g., DRAM).
|
||||
*/
|
||||
virtual void SetFastTier(UUID id) {}
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include "tiered_cache/scheduler/scheduler_policy.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @class SimplePolicy
|
||||
* @brief MVP Policy: Promote hot data to a designated "Fast Tier"
|
||||
*/
|
||||
class SimplePolicy : public SchedulerPolicy {
|
||||
public:
|
||||
struct Config {
|
||||
double promotion_threshold =
|
||||
10.0; // Min access count/score to trigger promotion
|
||||
};
|
||||
|
||||
explicit SimplePolicy(Config config);
|
||||
|
||||
// Configure which tier is considered "Fast" (Target for promotion)
|
||||
void SetFastTier(UUID id) override;
|
||||
|
||||
tl::expected<std::vector<SchedAction>, ErrorCode> Decide(
|
||||
const std::unordered_map<UUID, TierStats>& tier_stats,
|
||||
const std::vector<KeyContext>& active_keys) override;
|
||||
|
||||
private:
|
||||
Config config_;
|
||||
std::optional<UUID> fast_tier_id_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "tiered_cache/scheduler/scheduler_policy.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline size_t NormalizeShardCount(size_t shard_count) {
|
||||
size_t normalized = 1;
|
||||
while (normalized < std::max<size_t>(1, shard_count)) {
|
||||
normalized <<= 1;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
inline size_t DefaultStatsShardCount() {
|
||||
const auto hardware_threads =
|
||||
std::max<unsigned int>(1, std::thread::hardware_concurrency());
|
||||
return NormalizeShardCount(static_cast<size_t>(hardware_threads) * 4);
|
||||
}
|
||||
|
||||
inline size_t NormalizeSnapshotLimit(size_t limit) {
|
||||
return limit == 0 ? std::numeric_limits<size_t>::max() : limit;
|
||||
}
|
||||
|
||||
inline size_t DefaultSnapshotLimit() { return 4096; }
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* @enum AccessStatMetric
|
||||
* @brief Semantic meaning carried by a stats snapshot.
|
||||
*/
|
||||
enum class AccessStatMetric {
|
||||
kRecentHeat,
|
||||
kRecencyRank,
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct AccessStatEntry
|
||||
* @brief Per-key access metadata emitted by a stats collector.
|
||||
*/
|
||||
struct AccessStatEntry {
|
||||
std::string key;
|
||||
double recent_heat_score = 0.0;
|
||||
size_t recency_rank = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct AccessStats
|
||||
* @brief Snapshot of access statistics
|
||||
*/
|
||||
struct AccessStats {
|
||||
AccessStatMetric metric = AccessStatMetric::kRecentHeat;
|
||||
std::vector<AccessStatEntry> hot_keys;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class StatsCollector
|
||||
* @brief Interface for collecting runtime statistics
|
||||
*/
|
||||
class StatsCollector {
|
||||
public:
|
||||
virtual ~StatsCollector() = default;
|
||||
|
||||
// Record an access event for a key
|
||||
virtual void RecordAccess(const std::string& key) = 0;
|
||||
|
||||
// Get a snapshot of current stats for policy decision
|
||||
virtual AccessStats GetSnapshot() = 0;
|
||||
|
||||
// Remove a key from tracking (called when key is deleted)
|
||||
virtual void RemoveKey(const std::string& key) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class SimpleStatsCollector
|
||||
* @brief MVP implementation using thread-safe map counter with decay
|
||||
*/
|
||||
class SimpleStatsCollector : public StatsCollector {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
using NowFn = std::function<TimePoint()>;
|
||||
|
||||
// Decay factor per second: 0.5 means scores are halved every second.
|
||||
// This preserves history while giving more weight to recent accesses.
|
||||
explicit SimpleStatsCollector(
|
||||
double decay_factor_per_second = 0.5,
|
||||
size_t shard_count = detail::DefaultStatsShardCount(),
|
||||
size_t max_snapshot_keys = detail::DefaultSnapshotLimit(),
|
||||
NowFn now_fn = []() { return Clock::now(); })
|
||||
: shards_(detail::NormalizeShardCount(shard_count)),
|
||||
shard_mask_(shards_.size() - 1),
|
||||
max_snapshot_keys_(detail::NormalizeSnapshotLimit(max_snapshot_keys)),
|
||||
decay_factor_per_second_(
|
||||
NormalizeDecayFactor(decay_factor_per_second)),
|
||||
log_decay_factor_per_second_(std::log(decay_factor_per_second_)),
|
||||
now_fn_(std::move(now_fn)) {
|
||||
last_aggregate_update_time_ = now_fn_();
|
||||
}
|
||||
|
||||
void RecordAccess(const std::string& key) override {
|
||||
auto& shard = GetShard(key);
|
||||
std::lock_guard<std::mutex> lock(shard.mutex);
|
||||
shard.pending_counts[key]++;
|
||||
}
|
||||
|
||||
AccessStats GetSnapshot() override {
|
||||
const auto now = now_fn_();
|
||||
std::lock_guard<std::mutex> snapshot_lock(snapshot_mutex_);
|
||||
AdvanceAggregateTo(now);
|
||||
RebaseScoresIfNeeded();
|
||||
ApplyPendingChanges();
|
||||
AccessStats stats = BuildSnapshot();
|
||||
PruneColdKeys();
|
||||
return stats;
|
||||
}
|
||||
|
||||
void RemoveKey(const std::string& key) override {
|
||||
auto& shard = GetShard(key);
|
||||
std::lock_guard<std::mutex> lock(shard.mutex);
|
||||
shard.pending_counts.erase(key);
|
||||
shard.pending_deletes.push_back(key);
|
||||
}
|
||||
|
||||
private:
|
||||
struct OrderedKey {
|
||||
double raw_score;
|
||||
std::string key;
|
||||
};
|
||||
|
||||
struct OrderedKeyCompare {
|
||||
bool operator()(const OrderedKey& lhs, const OrderedKey& rhs) const {
|
||||
if (lhs.raw_score != rhs.raw_score) {
|
||||
return lhs.raw_score > rhs.raw_score;
|
||||
}
|
||||
return lhs.key < rhs.key;
|
||||
}
|
||||
};
|
||||
|
||||
struct alignas(64) Shard {
|
||||
std::mutex mutex;
|
||||
std::unordered_map<std::string, uint64_t> pending_counts;
|
||||
std::vector<std::string> pending_deletes;
|
||||
};
|
||||
|
||||
Shard& GetShard(const std::string& key) {
|
||||
const auto shard_index = std::hash<std::string>{}(key)&shard_mask_;
|
||||
return shards_[shard_index];
|
||||
}
|
||||
|
||||
void ApplyPendingChanges() {
|
||||
for (auto& shard : shards_) {
|
||||
std::unordered_map<std::string, uint64_t> pending_counts;
|
||||
std::vector<std::string> pending_deletes;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shard.mutex);
|
||||
pending_counts.swap(shard.pending_counts);
|
||||
pending_deletes.swap(shard.pending_deletes);
|
||||
}
|
||||
|
||||
for (const auto& key : pending_deletes) {
|
||||
RemoveAggregateKey(key);
|
||||
}
|
||||
|
||||
for (const auto& [key, count] : pending_counts) {
|
||||
AddCount(key, static_cast<double>(count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AddCount(const std::string& key, double actual_score_delta) {
|
||||
if (actual_score_delta <= 0.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double delta = actual_score_delta / global_scale_;
|
||||
auto it = raw_counts_.find(key);
|
||||
if (it != raw_counts_.end()) {
|
||||
ordered_keys_.erase(OrderedKey{it->second, key});
|
||||
it->second += delta;
|
||||
ordered_keys_.insert(OrderedKey{it->second, key});
|
||||
return;
|
||||
}
|
||||
|
||||
raw_counts_[key] = delta;
|
||||
ordered_keys_.insert(OrderedKey{delta, key});
|
||||
}
|
||||
|
||||
void RemoveAggregateKey(const std::string& key) {
|
||||
auto it = raw_counts_.find(key);
|
||||
if (it == raw_counts_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ordered_keys_.erase(OrderedKey{it->second, key});
|
||||
raw_counts_.erase(it);
|
||||
}
|
||||
|
||||
AccessStats BuildSnapshot() const {
|
||||
AccessStats stats;
|
||||
stats.metric = AccessStatMetric::kRecentHeat;
|
||||
stats.hot_keys.reserve(
|
||||
std::min(max_snapshot_keys_, ordered_keys_.size()));
|
||||
|
||||
size_t emitted = 0;
|
||||
for (const auto& ordered_key : ordered_keys_) {
|
||||
const double score = ordered_key.raw_score * global_scale_;
|
||||
if (score < kMinTrackedScore) {
|
||||
break;
|
||||
}
|
||||
|
||||
stats.hot_keys.push_back(
|
||||
AccessStatEntry{ordered_key.key, score, 0});
|
||||
emitted++;
|
||||
if (emitted >= max_snapshot_keys_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
void AdvanceAggregateTo(TimePoint now) {
|
||||
global_scale_ =
|
||||
AdvanceScale(global_scale_, last_aggregate_update_time_, now);
|
||||
}
|
||||
|
||||
double AdvanceScale(double scale, TimePoint& last_update_time,
|
||||
TimePoint now) const {
|
||||
if (now <= last_update_time) {
|
||||
return scale;
|
||||
}
|
||||
|
||||
const double elapsed_seconds =
|
||||
std::chrono::duration<double>(now - last_update_time).count();
|
||||
last_update_time = now;
|
||||
if (log_decay_factor_per_second_ == 0.0) {
|
||||
return scale;
|
||||
}
|
||||
|
||||
return scale * std::exp(log_decay_factor_per_second_ * elapsed_seconds);
|
||||
}
|
||||
|
||||
void RebaseScoresIfNeeded() {
|
||||
if (global_scale_ >= kMinScaleBeforeRebase) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw_counts_.empty()) {
|
||||
global_scale_ = 1.0;
|
||||
return;
|
||||
}
|
||||
|
||||
std::set<OrderedKey, OrderedKeyCompare> rebased_order;
|
||||
for (auto& [key, raw_score] : raw_counts_) {
|
||||
raw_score *= global_scale_;
|
||||
rebased_order.insert(OrderedKey{raw_score, key});
|
||||
}
|
||||
|
||||
ordered_keys_.swap(rebased_order);
|
||||
global_scale_ = 1.0;
|
||||
}
|
||||
|
||||
void PruneColdKeys() {
|
||||
while (!ordered_keys_.empty()) {
|
||||
auto coldest_it = std::prev(ordered_keys_.end());
|
||||
if (coldest_it->raw_score * global_scale_ >= kMinTrackedScore) {
|
||||
break;
|
||||
}
|
||||
|
||||
raw_counts_.erase(coldest_it->key);
|
||||
ordered_keys_.erase(coldest_it);
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr double kMinTrackedScore = 1.0;
|
||||
static constexpr double kMinScaleBeforeRebase = 1e-100;
|
||||
|
||||
static double NormalizeDecayFactor(double decay_factor_per_second) {
|
||||
if (!std::isfinite(decay_factor_per_second) ||
|
||||
decay_factor_per_second <= 0.0) {
|
||||
return std::numeric_limits<double>::min();
|
||||
}
|
||||
return std::min(decay_factor_per_second, 1.0);
|
||||
}
|
||||
|
||||
std::mutex snapshot_mutex_;
|
||||
std::vector<Shard> shards_;
|
||||
size_t shard_mask_;
|
||||
std::unordered_map<std::string, double> raw_counts_;
|
||||
std::set<OrderedKey, OrderedKeyCompare> ordered_keys_;
|
||||
size_t max_snapshot_keys_;
|
||||
TimePoint last_aggregate_update_time_{};
|
||||
double global_scale_ = 1.0;
|
||||
double decay_factor_per_second_;
|
||||
double log_decay_factor_per_second_;
|
||||
NowFn now_fn_;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <shared_mutex>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <json/value.h>
|
||||
|
||||
#include "tiered_cache/tiers/cache_tier.h"
|
||||
#include "tiered_cache/data_copier.h"
|
||||
#include "tiered_cache/scheduler/stats_collector.h"
|
||||
#include "rpc_types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class TieredBackend; // Forward declaration
|
||||
class ClientScheduler; // Forward declaration
|
||||
|
||||
/**
|
||||
* @struct TieredLocation
|
||||
* @brief Describes the physical location of a segment within the tiered
|
||||
* storage.
|
||||
*/
|
||||
struct TieredLocation {
|
||||
std::shared_ptr<CacheTier> tier;
|
||||
struct DataSource data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct TierView
|
||||
* @brief A snapshot of a tier's status, used for reporting topology to the
|
||||
* Master.
|
||||
*/
|
||||
struct TierView {
|
||||
UUID id;
|
||||
MemoryType type;
|
||||
size_t capacity;
|
||||
size_t usage;
|
||||
size_t free_space;
|
||||
int priority;
|
||||
std::vector<std::string> tags;
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum REMOVE_CALLBACK_TYPE
|
||||
* @brief The type of metadata synchronization callback.
|
||||
*/
|
||||
enum REMOVE_CALLBACK_TYPE { DELETE = 0, DELETE_ALL = 1 };
|
||||
|
||||
/**
|
||||
* @struct AllocationEntry
|
||||
* @brief The internal state of an allocation.
|
||||
* acts as the "Control Block" for the resource.
|
||||
* When the last shared_ptr pointing to this entry dies, the destructor
|
||||
* releases the resource through the owning tier.
|
||||
*/
|
||||
struct AllocationEntry {
|
||||
TieredBackend* backend;
|
||||
TieredLocation loc;
|
||||
|
||||
AllocationEntry(TieredBackend* b, TieredLocation&& l)
|
||||
: backend(b), loc(std::move(l)) {}
|
||||
AllocationEntry(const AllocationEntry&) = delete;
|
||||
AllocationEntry& operator=(const AllocationEntry&) = delete;
|
||||
|
||||
// Destructor: Automatically releases the resource if valid
|
||||
~AllocationEntry();
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef AllocationHandle
|
||||
* @brief A reference-counted handle to a storage resource.
|
||||
*/
|
||||
using AllocationHandle = std::shared_ptr<AllocationEntry>;
|
||||
|
||||
/**
|
||||
* @brief Callback for metadata synchronization when a replica is added.
|
||||
* Invoked after data copy is complete.
|
||||
* Returns true if sync succeeds, false otherwise.
|
||||
*/
|
||||
using AddReplicaCallback = std::function<tl::expected<void, ErrorCode>(
|
||||
const std::string& key, const UUID& tier_id, size_t size)>;
|
||||
|
||||
/**
|
||||
* @brief Callback for metadata synchronization when a replica is removed.
|
||||
* Returns true if sync succeeds, false otherwise.
|
||||
*/
|
||||
using RemoveReplicaCallback = std::function<tl::expected<void, ErrorCode>(
|
||||
const std::string& key, const UUID& tier_id,
|
||||
enum REMOVE_CALLBACK_TYPE type)>;
|
||||
|
||||
/**
|
||||
* @brief Callback for segment lifecycle synchronization.
|
||||
* Invoked when a tier is created (mount=true) or destroyed (mount=false).
|
||||
* The callback should register/unregister the segment with Master.
|
||||
*/
|
||||
using SegmentSyncCallback = std::function<tl::expected<void, ErrorCode>(
|
||||
const Segment& segment, bool mount)>;
|
||||
|
||||
/**
|
||||
* @class TieredBackend
|
||||
* @brief Data plane management class supporting tiered storage with RAII-based
|
||||
* resource management.
|
||||
*/
|
||||
class TieredBackend {
|
||||
public:
|
||||
TieredBackend();
|
||||
~TieredBackend();
|
||||
|
||||
/**
|
||||
* @brief 1. stops any backend thread;
|
||||
* 2. all public APIs will return SHUTTING_DOWN.
|
||||
*/
|
||||
void Stop();
|
||||
|
||||
/**
|
||||
* @brief Unmounts segments from Master and cleans up resources.
|
||||
*/
|
||||
void Destroy();
|
||||
|
||||
tl::expected<void, ErrorCode> Init(
|
||||
Json::Value root, TransferEngine* engine,
|
||||
AddReplicaCallback add_replica_callback,
|
||||
RemoveReplicaCallback remove_replica_callback,
|
||||
SegmentSyncCallback segment_sync_callback);
|
||||
|
||||
// --- Client-Centric Operations ---
|
||||
// All the following operations are designed for Client-Centric, Client
|
||||
// should manage the resource by itself, and synchronize with Master when
|
||||
// needed.
|
||||
|
||||
/**
|
||||
* @brief Allocation
|
||||
* reserves storage space. Returns a handle.
|
||||
* If the handle goes out of scope without being committed, the space is
|
||||
* auto-freed.
|
||||
* @param size: Size in bytes to allocate
|
||||
* @param preferred_tier: Preferred tier ID (optional)
|
||||
* @param strict: If true, allocation MUST succeed on preferred_tier.
|
||||
* Will trigger sync eviction if needed, no fallback.
|
||||
* If false (default), will fallback to other tiers.
|
||||
*/
|
||||
tl::expected<AllocationHandle, ErrorCode> Allocate(
|
||||
size_t size, std::optional<UUID> preferred_tier = std::nullopt,
|
||||
bool strict = false);
|
||||
|
||||
/**
|
||||
* @brief Execution (Write)
|
||||
* Writes data to the location specified by the handle.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Write(const DataSource& source,
|
||||
AllocationHandle handle);
|
||||
|
||||
/**
|
||||
* @brief Commit (Register)
|
||||
* Registers the handle in the local metadata index.
|
||||
* @param expected_version: Optimistic Concurrency Control.
|
||||
* If set, commit only if current version matches expected_version.
|
||||
* Returns CAS_FAILED if mismatch.
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Commit(
|
||||
const std::string& key, AllocationHandle handle,
|
||||
std::optional<uint64_t> expected_version = std::nullopt,
|
||||
bool record_access = true);
|
||||
|
||||
/**
|
||||
* @brief Checks if a key exists in the backend.
|
||||
* @param key The key to check.
|
||||
* @param tier_id Optional tier ID. If specified, checks only the given
|
||||
* tier; if nullopt, checks any tier.
|
||||
*/
|
||||
bool Exist(const std::string& key,
|
||||
std::optional<UUID> tier_id = std::nullopt) const;
|
||||
|
||||
/**
|
||||
* @brief Get
|
||||
* Returns a handle.
|
||||
* @param out_version: If provided, returns the current version of the
|
||||
* metadata entry.
|
||||
*/
|
||||
tl::expected<AllocationHandle, ErrorCode> Get(
|
||||
const std::string& key, std::optional<UUID> tier_id = std::nullopt,
|
||||
bool record_access = true, uint64_t* out_version = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Delete
|
||||
* Removes the key from the metadata index.
|
||||
* @param tier_id: If specified, removes only the replica on that tier.
|
||||
* If nullopt, removes ALL replicas for this key (and the key entry itself).
|
||||
*/
|
||||
tl::expected<void, ErrorCode> Delete(
|
||||
const std::string& key, std::optional<UUID> tier_id = std::nullopt);
|
||||
|
||||
// --- Composite Operations ---
|
||||
|
||||
tl::expected<void, ErrorCode> CopyData(
|
||||
const std::string& key, const DataSource& source, UUID dest_tier_id,
|
||||
std::optional<uint64_t> expected_version = std::nullopt,
|
||||
bool record_access = true);
|
||||
|
||||
tl::expected<void, ErrorCode> Transfer(const std::string& key,
|
||||
UUID source_tier_id,
|
||||
UUID dest_tier_id,
|
||||
bool record_access = true);
|
||||
|
||||
// --- Introspection & Internal ---
|
||||
|
||||
std::vector<TierView> GetTierViews() const;
|
||||
std::vector<UUID> GetReplicaTierIds(const std::string& key) const;
|
||||
const CacheTier* GetTier(UUID tier_id) const;
|
||||
const DataCopier& GetDataCopier() const;
|
||||
|
||||
/**
|
||||
* @brief Iterate all keys in batches.
|
||||
* Iterates per-shard to minimize lock hold time.
|
||||
* @param callback Receives each batch; return false to stop iteration.
|
||||
*/
|
||||
void ForEachKeyBatch(
|
||||
const std::function<bool(std::vector<ReplicaLocation>&&)>& callback)
|
||||
const;
|
||||
|
||||
/**
|
||||
* @brief Get hot key statistics from the scheduler's StatsCollector.
|
||||
*/
|
||||
AccessStats GetHotKeyStats() const;
|
||||
|
||||
private:
|
||||
tl::expected<void, ErrorCode> MountSegment(
|
||||
UUID id, size_t capacity, int priority,
|
||||
const std::vector<std::string>& tags, MemoryType memory_type);
|
||||
|
||||
struct TierInfo {
|
||||
int priority;
|
||||
std::vector<std::string> tags;
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct MetadataEntry
|
||||
* @brief Holds all replicas for a specific key.
|
||||
* Uses a dedicated mutex to allow per-key concurrency.
|
||||
*/
|
||||
struct MetadataEntry {
|
||||
mutable std::shared_mutex mutex; // Entry-level lock
|
||||
std::vector<std::pair<UUID, AllocationHandle>>
|
||||
replicas; // tier_id -> handle
|
||||
uint64_t version = 0; // Monotonically increasing version
|
||||
};
|
||||
|
||||
// Get list of Tier IDs sorted by priority (descending)
|
||||
std::vector<UUID> GetSortedTiers() const;
|
||||
|
||||
// Low-level allocation logic
|
||||
tl::expected<void, ErrorCode> AllocateInternalRaw(
|
||||
size_t size, std::optional<UUID> preferred_tier,
|
||||
TieredLocation* out_loc);
|
||||
|
||||
private:
|
||||
// Map from tier ID to the actual CacheTier instance.
|
||||
std::unordered_map<UUID, std::shared_ptr<CacheTier>> tiers_;
|
||||
|
||||
// Map from tier ID to static config info
|
||||
std::unordered_map<UUID, TierInfo> tier_info_;
|
||||
|
||||
// Sharded Metadata Index: Key -> Entry
|
||||
// Each shard has its own mutex for fine-grained locking.
|
||||
struct MetadataShard {
|
||||
mutable std::shared_mutex mutex;
|
||||
std::unordered_map<std::string, std::shared_ptr<MetadataEntry>> index;
|
||||
};
|
||||
|
||||
static constexpr size_t kMetadataShardCount = 64;
|
||||
std::array<MetadataShard, kMetadataShardCount> metadata_shards_;
|
||||
|
||||
MetadataShard& GetMetadataShard(const std::string& key) {
|
||||
return metadata_shards_[std::hash<std::string>{}(key) %
|
||||
kMetadataShardCount];
|
||||
}
|
||||
const MetadataShard& GetMetadataShard(const std::string& key) const {
|
||||
return metadata_shards_[std::hash<std::string>{}(key) %
|
||||
kMetadataShardCount];
|
||||
}
|
||||
|
||||
std::unique_ptr<DataCopier> data_copier_;
|
||||
// Callbacks for metadata synchronization with Master
|
||||
AddReplicaCallback add_replica_callback_;
|
||||
RemoveReplicaCallback remove_replica_callback_;
|
||||
// Callback for segment lifecycle synchronization with Master
|
||||
SegmentSyncCallback segment_sync_callback_;
|
||||
|
||||
// Scheduler
|
||||
std::unique_ptr<ClientScheduler> scheduler_;
|
||||
|
||||
// Shutdown flag — once set, all public APIs reject new requests.
|
||||
std::atomic<bool> is_shutting_down_{false};
|
||||
|
||||
// Destroy flag
|
||||
std::atomic<bool> is_destroyed_{false};
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "tiered_cache/tiers/cache_tier.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @struct AscendUnifiedPointer
|
||||
* @brief Encapsulates Ascend device pointer information.
|
||||
*
|
||||
* Contains the device pointer along with device ID and size,
|
||||
* enabling proper device context management for multi-device scenarios.
|
||||
*/
|
||||
struct AscendUnifiedPointer {
|
||||
void* device_ptr; // The actual device memory pointer
|
||||
int device_id; // The device ID this pointer belongs to
|
||||
size_t size; // Size of the allocated memory in bytes
|
||||
};
|
||||
|
||||
/**
|
||||
* @class AscendBuffer
|
||||
* @brief Ascend NPU memory buffer wrapper inheriting from BufferBase.
|
||||
*
|
||||
* Implements RAII pattern for automatic device memory management.
|
||||
* When the buffer goes out of scope, device memory is automatically freed.
|
||||
*/
|
||||
class AscendBuffer : public BufferBase {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs an AscendBuffer taking ownership of device memory.
|
||||
* @param unified_ptr unique_ptr to AscendUnifiedPointer containing device
|
||||
* memory info.
|
||||
*/
|
||||
explicit AscendBuffer(std::unique_ptr<AscendUnifiedPointer> unified_ptr);
|
||||
|
||||
/**
|
||||
* @brief Destructor that automatically releases device memory.
|
||||
*/
|
||||
~AscendBuffer() override;
|
||||
|
||||
// Disable copy operations
|
||||
AscendBuffer(const AscendBuffer&) = delete;
|
||||
AscendBuffer& operator=(const AscendBuffer&) = delete;
|
||||
|
||||
// Enable move operations
|
||||
AscendBuffer(AscendBuffer&& other) noexcept;
|
||||
AscendBuffer& operator=(AscendBuffer&& other) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Returns the device pointer address as uint64_t.
|
||||
*
|
||||
* Returns the raw device memory pointer. For accessing device context
|
||||
* information (device_id), use GetUnifiedPointer() or GetDeviceId().
|
||||
*/
|
||||
uint64_t data() const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the size of the allocated memory.
|
||||
*/
|
||||
std::size_t size() const override;
|
||||
|
||||
/**
|
||||
* @brief Gets the device ID for this buffer.
|
||||
* @return Device ID, or -1 if buffer is invalid.
|
||||
*/
|
||||
int GetDeviceId() const;
|
||||
|
||||
/**
|
||||
* @brief Gets the actual device pointer.
|
||||
* @return Device pointer, or nullptr if buffer is invalid.
|
||||
*/
|
||||
void* GetDevicePtr() const;
|
||||
|
||||
/**
|
||||
* @brief Gets the complete AscendUnifiedPointer structure.
|
||||
* @return Pointer to AscendUnifiedPointer, or nullptr if invalid.
|
||||
*/
|
||||
const AscendUnifiedPointer* GetUnifiedPointer() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<AscendUnifiedPointer> unified_ptr_;
|
||||
|
||||
// Internal function to release device memory
|
||||
void ReleaseMemory();
|
||||
};
|
||||
|
||||
/**
|
||||
* @class AscendCacheTier
|
||||
* @brief Ascend NPU cache tier implementation for the new CacheTier interface.
|
||||
*
|
||||
* Provides device memory allocation and management for Huawei Ascend NPU
|
||||
* devices. Supports the Allocate/Free resource management pattern with RAII
|
||||
* semantics.
|
||||
*/
|
||||
class AscendCacheTier : public CacheTier {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs an AscendCacheTier.
|
||||
* @param tier_id Unique identifier for this tier (UUID).
|
||||
* @param capacity Total capacity in bytes.
|
||||
* @param tags Optional tags for tier identification.
|
||||
* @param device_id Ascend device ID (default: 0).
|
||||
*/
|
||||
AscendCacheTier(UUID tier_id, size_t capacity,
|
||||
const std::vector<std::string>& tags, int device_id = 0);
|
||||
|
||||
~AscendCacheTier() override;
|
||||
|
||||
// Disable copy
|
||||
AscendCacheTier(const AscendCacheTier&) = delete;
|
||||
AscendCacheTier& operator=(const AscendCacheTier&) = delete;
|
||||
|
||||
// CacheTier interface implementation
|
||||
tl::expected<void, ErrorCode> Init(TieredBackend* backend,
|
||||
TransferEngine* engine) override;
|
||||
|
||||
tl::expected<void, ErrorCode> Allocate(size_t size,
|
||||
DataSource& data) override;
|
||||
|
||||
tl::expected<void, ErrorCode> Free(DataSource data) override;
|
||||
|
||||
// Accessors
|
||||
UUID GetTierId() const override { return tier_id_; }
|
||||
size_t GetCapacity() const override { return capacity_; }
|
||||
size_t GetUsage() const override;
|
||||
MemoryType GetMemoryType() const override { return MemoryType::ASCEND_NPU; }
|
||||
const std::vector<std::string>& GetTags() const override { return tags_; }
|
||||
|
||||
/**
|
||||
* @brief Gets the device ID for this cache tier.
|
||||
*/
|
||||
int GetDeviceId() const { return device_id_; }
|
||||
|
||||
private:
|
||||
UUID tier_id_;
|
||||
size_t capacity_;
|
||||
std::vector<std::string> tags_;
|
||||
int device_id_;
|
||||
|
||||
// Memory usage tracking (atomic for thread safety)
|
||||
std::atomic<size_t> current_usage_{0};
|
||||
|
||||
// Initialization state
|
||||
bool is_initialized_{false};
|
||||
mutable std::mutex init_mutex_;
|
||||
|
||||
// Internal device memory allocation
|
||||
std::unique_ptr<AscendUnifiedPointer> AllocateDeviceMemory(size_t size);
|
||||
|
||||
// Check if sufficient space is available
|
||||
bool HasSpace(size_t size) const;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <ylt/util/tl/expected.hpp>
|
||||
|
||||
#include "allocator.h"
|
||||
#include "transfer_engine.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class TieredBackend;
|
||||
|
||||
/**
|
||||
* @class BufferBase
|
||||
* @brief Base class for different types of memory buffers
|
||||
*/
|
||||
class BufferBase {
|
||||
public:
|
||||
virtual ~BufferBase() = default;
|
||||
virtual uint64_t data() const = 0;
|
||||
virtual std::size_t size() const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class DRAMBuffer
|
||||
* @brief Wrapper for DRAM AllocatedBuffer
|
||||
*/
|
||||
class DRAMBuffer : public BufferBase {
|
||||
public:
|
||||
explicit DRAMBuffer(std::unique_ptr<AllocatedBuffer> buffer)
|
||||
: dram_buffer_(std::move(buffer)) {}
|
||||
|
||||
uint64_t data() const override {
|
||||
return dram_buffer_ ? reinterpret_cast<uint64_t>(dram_buffer_->data())
|
||||
: 0;
|
||||
}
|
||||
|
||||
std::size_t size() const override {
|
||||
return dram_buffer_ ? dram_buffer_->size() : 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<AllocatedBuffer> dram_buffer_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class TempDRAMBuffer
|
||||
* @brief Wrapper for temporary DRAM buffers with RAII memory management
|
||||
*/
|
||||
class TempDRAMBuffer : public BufferBase {
|
||||
public:
|
||||
// Constructor that takes ownership of the buffer
|
||||
explicit TempDRAMBuffer(std::unique_ptr<char[]> buffer, size_t size)
|
||||
: buffer_(std::move(buffer)), size_(size) {}
|
||||
|
||||
uint64_t data() const override {
|
||||
return reinterpret_cast<uint64_t>(buffer_.get());
|
||||
}
|
||||
std::size_t size() const override { return size_; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<char[]>
|
||||
buffer_; // Owns the memory, auto-releases on destruction
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class RefBuffer
|
||||
* @brief Helper class to wrap a raw pointer as BufferBase without taking
|
||||
* ownership This is used for DataCopier operations where the source memory is
|
||||
* owned elsewhere
|
||||
*/
|
||||
class RefBuffer : public BufferBase {
|
||||
public:
|
||||
explicit RefBuffer(void* ptr, size_t size) : ptr_(ptr), size_(size) {}
|
||||
|
||||
uint64_t data() const override { return reinterpret_cast<uint64_t>(ptr_); }
|
||||
|
||||
std::size_t size() const override { return size_; }
|
||||
|
||||
private:
|
||||
void* ptr_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct DataSource
|
||||
* @brief Describes a source of data for copy/write operations.
|
||||
*/
|
||||
struct DataSource {
|
||||
std::unique_ptr<BufferBase> buffer;
|
||||
MemoryType type; // Source memory type
|
||||
};
|
||||
|
||||
/**
|
||||
* @class CacheTier
|
||||
* @brief Abstract base class for a single tier (e.g., DRAM, SSD).
|
||||
* * Update: Supports decoupled Allocation/Write/Bind operations to allow
|
||||
* flexible placement strategies (Client-centric vs Master-centric).
|
||||
*/
|
||||
class CacheTier {
|
||||
public:
|
||||
virtual ~CacheTier() = default;
|
||||
|
||||
/**
|
||||
* @brief Initializes the cache tier.
|
||||
* @return tl::expected<void, ErrorCode> indicating success or error code.
|
||||
*/
|
||||
virtual tl::expected<void, ErrorCode> Init(TieredBackend* backend,
|
||||
TransferEngine* engine) = 0;
|
||||
|
||||
/**
|
||||
* @brief Reserve Space (Allocation)
|
||||
* Finds free space of `size` bytes. Does NOT copy data.
|
||||
* @param size Bytes to allocate.
|
||||
* @param data DataSource struct to fill with allocation info.
|
||||
* @return tl::expected<void, ErrorCode> indicating success or error code.
|
||||
*/
|
||||
virtual tl::expected<void, ErrorCode> Allocate(size_t size,
|
||||
DataSource& data) = 0;
|
||||
|
||||
/**
|
||||
* @brief Free Space (Rollback/Cleanup)
|
||||
* Releases space at offset. Used when writes fail or explicitly freeing
|
||||
* anonymous blocks.
|
||||
* @return tl::expected<void, ErrorCode> indicating success or error code.
|
||||
*/
|
||||
virtual tl::expected<void, ErrorCode> Free(DataSource data) = 0;
|
||||
|
||||
// --- Accessors & Metadata ---
|
||||
virtual UUID GetTierId() const = 0;
|
||||
virtual size_t GetCapacity() const = 0;
|
||||
virtual size_t GetUsage() const = 0;
|
||||
virtual MemoryType GetMemoryType() const = 0;
|
||||
virtual const std::vector<std::string>& GetTags() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Commit (Register)
|
||||
* Registers the key and finalizes the storage.
|
||||
* For Storage Tiers, this triggers the actual persistence (or buffering).
|
||||
*/
|
||||
virtual tl::expected<void, ErrorCode> Commit(const std::string& key,
|
||||
const DataSource& data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Flush
|
||||
* Forces any buffered data to be persisted to the underlying storage.
|
||||
*/
|
||||
virtual tl::expected<void, ErrorCode> Flush() { return {}; }
|
||||
|
||||
protected:
|
||||
// A pointer to the parent backend, allowing tiers to access shared services
|
||||
// like the DataCopier.
|
||||
TieredBackend* backend_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
|
||||
#include "allocator.h"
|
||||
#include "tiered_cache/tiers/cache_tier.h"
|
||||
#include "transfer_engine.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class DramCacheTier : public CacheTier {
|
||||
public:
|
||||
DramCacheTier(
|
||||
UUID tier_id, size_t capacity, const std::vector<std::string>& tags,
|
||||
std::optional<int> numa_node = std::nullopt,
|
||||
BufferAllocatorType allocator_type = BufferAllocatorType::OFFSET);
|
||||
~DramCacheTier() override;
|
||||
|
||||
tl::expected<void, ErrorCode> Init(TieredBackend* backend,
|
||||
TransferEngine* engine) override;
|
||||
tl::expected<void, ErrorCode> Allocate(size_t size,
|
||||
DataSource& data) override;
|
||||
tl::expected<void, ErrorCode> Free(DataSource data) override;
|
||||
|
||||
UUID GetTierId() const override { return tier_id_; }
|
||||
size_t GetCapacity() const override { return capacity_; }
|
||||
size_t GetUsage() const override;
|
||||
const std::vector<std::string>& GetTags() const override { return tags_; }
|
||||
MemoryType GetMemoryType() const override { return MemoryType::DRAM; }
|
||||
|
||||
private:
|
||||
UUID tier_id_;
|
||||
size_t capacity_;
|
||||
std::vector<std::string> tags_;
|
||||
std::optional<int> numa_node_;
|
||||
BufferAllocatorType allocator_type_;
|
||||
std::shared_ptr<BufferAllocatorBase> allocator_;
|
||||
TransferEngine* engine_;
|
||||
std::unique_ptr<char[], void (*)(char*)> memory_buffer_{
|
||||
nullptr, [](char* p) { delete[] p; }};
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <cstring>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
|
||||
#include "tiered_cache/tiers/cache_tier.h"
|
||||
#include "storage_backend.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
/**
|
||||
* @class StorageBuffer
|
||||
* @brief A unified buffer for storage tier that handles both staging (DRAM)
|
||||
* and persisted (Disk) states.
|
||||
*/
|
||||
class StorageBuffer : public BufferBase {
|
||||
public:
|
||||
explicit StorageBuffer(std::unique_ptr<AllocatedBuffer> staging_buffer,
|
||||
StorageBackendInterface* backend)
|
||||
: staging_buffer_(std::move(staging_buffer)),
|
||||
backend_(backend),
|
||||
size_(staging_buffer_ ? staging_buffer_->size() : 0) {}
|
||||
|
||||
uint64_t data() const override {
|
||||
// Return valid pointer only while data is still in the staging pool.
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
if (!is_on_disk_.load(std::memory_order_acquire) && staging_buffer_) {
|
||||
return reinterpret_cast<uint64_t>(staging_buffer_->data());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::size_t size() const override { return size_; }
|
||||
|
||||
// Helper to get raw pointer (only valid in staging mode).
|
||||
char* data_ptr() {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return is_on_disk_.load(std::memory_order_acquire) || !staging_buffer_
|
||||
? nullptr
|
||||
: static_cast<char*>(staging_buffer_->data());
|
||||
}
|
||||
|
||||
const char* data_ptr() const {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return is_on_disk_.load(std::memory_order_acquire) || !staging_buffer_
|
||||
? nullptr
|
||||
: static_cast<const char*>(staging_buffer_->data());
|
||||
}
|
||||
|
||||
Slice ToSlice() const {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
if (is_on_disk_.load(std::memory_order_acquire) || !staging_buffer_) {
|
||||
return Slice{nullptr, size_};
|
||||
}
|
||||
return Slice{static_cast<char*>(staging_buffer_->data()), size_};
|
||||
}
|
||||
|
||||
void SetKey(const std::string& key) { key_ = key; }
|
||||
const std::string& GetKey() const { return key_; }
|
||||
|
||||
// Transition from staging pool -> on disk.
|
||||
void Persist() {
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
if (is_on_disk_.load(std::memory_order_acquire)) return;
|
||||
if (staging_buffer_) {
|
||||
size_ = staging_buffer_->size();
|
||||
staging_buffer_.reset();
|
||||
}
|
||||
is_on_disk_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool IsPersisted() const {
|
||||
return is_on_disk_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void SetFlushing(bool flushing) {
|
||||
is_flushing_.store(flushing, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool IsFlushing() const {
|
||||
return is_flushing_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// Read data to destination buffer (handles staging vs disk transparently).
|
||||
tl::expected<void, ErrorCode> ReadTo(void* dst, size_t length) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
if (!is_on_disk_.load(std::memory_order_acquire)) {
|
||||
if (!staging_buffer_) {
|
||||
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
||||
}
|
||||
if (length > size_) {
|
||||
return tl::make_unexpected(ErrorCode::BUFFER_OVERFLOW);
|
||||
}
|
||||
std::memcpy(dst, staging_buffer_->data(), length);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// On disk: load from backend without holding the staging lock.
|
||||
if (!backend_ || key_.empty()) {
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, Slice> batch;
|
||||
batch[key_] = Slice{static_cast<char*>(dst), length};
|
||||
return backend_->BatchLoad(batch);
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex data_mutex_;
|
||||
std::unique_ptr<AllocatedBuffer> staging_buffer_;
|
||||
StorageBackendInterface* backend_ = nullptr;
|
||||
std::string key_;
|
||||
std::atomic<bool> is_on_disk_{false};
|
||||
std::atomic<bool> is_flushing_{false};
|
||||
size_t size_ = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class StorageTier
|
||||
* @brief Storage tier implementation for SSD/NVMe storage.
|
||||
*/
|
||||
class StorageTier : public CacheTier {
|
||||
public:
|
||||
StorageTier(UUID tier_id, const std::vector<std::string>& tags,
|
||||
size_t capacity = 0);
|
||||
|
||||
~StorageTier() override;
|
||||
|
||||
tl::expected<void, ErrorCode> Init(TieredBackend* backend,
|
||||
TransferEngine* engine) override;
|
||||
|
||||
tl::expected<void, ErrorCode> Allocate(size_t size,
|
||||
DataSource& data) override;
|
||||
|
||||
tl::expected<void, ErrorCode> Free(DataSource data) override;
|
||||
|
||||
tl::expected<void, ErrorCode> Commit(const std::string& key,
|
||||
const DataSource& data) override;
|
||||
|
||||
tl::expected<void, ErrorCode> Flush() override;
|
||||
|
||||
// --- Accessors ---
|
||||
UUID GetTierId() const override { return tier_id_; }
|
||||
size_t GetCapacity() const override;
|
||||
size_t GetUsage() const override;
|
||||
MemoryType GetMemoryType() const override { return MemoryType::NVME; }
|
||||
const std::vector<std::string>& GetTags() const override { return tags_; }
|
||||
|
||||
/**
|
||||
* @brief Trigger bucket eviction to free up space.
|
||||
* @param target_free_size Target amount of space to free (in bytes).
|
||||
* If 0, evicts one bucket.
|
||||
* @return tl::expected<size_t, ErrorCode>
|
||||
* - On success: total amount of space freed (in bytes)
|
||||
* - On failure: error code
|
||||
*/
|
||||
tl::expected<size_t, ErrorCode> TriggerBucketEviction(
|
||||
size_t target_free_size = 0);
|
||||
|
||||
private:
|
||||
static void FreeStagingMemory(char* ptr);
|
||||
|
||||
// Internal flush logic that triggers BatchOffload
|
||||
tl::expected<void, ErrorCode> FlushInternal();
|
||||
|
||||
// Background flush thread worker
|
||||
void FlushWorker();
|
||||
|
||||
UUID tier_id_;
|
||||
std::vector<std::string> tags_;
|
||||
|
||||
std::shared_ptr<StorageBackendInterface> storage_backend_;
|
||||
|
||||
// Pending Write Buffer for aggregation
|
||||
std::mutex batch_mutex_;
|
||||
std::condition_variable flush_cv_; // Signaled when flush completes
|
||||
std::unordered_map<std::string, StorageBuffer*> pending_batch_;
|
||||
std::atomic<size_t> pending_batch_size_{0};
|
||||
|
||||
// Async flush thread
|
||||
std::thread flush_thread_;
|
||||
std::atomic<bool> stop_flush_thread_{false};
|
||||
std::condition_variable flush_trigger_cv_;
|
||||
std::mutex flush_trigger_mutex_;
|
||||
std::atomic<bool> flush_requested_{false};
|
||||
|
||||
// Configurable thresholds
|
||||
size_t batch_size_threshold_ = 64 * 1024 * 1024; // 64MB
|
||||
size_t batch_count_threshold_ = 1000;
|
||||
|
||||
// Preallocated local staging pool for SSD writes.
|
||||
std::shared_ptr<BufferAllocatorBase> staging_allocator_;
|
||||
std::unique_ptr<char, void (*)(char*)> staging_memory_{
|
||||
nullptr, &StorageTier::FreeStagingMemory};
|
||||
size_t staging_buffer_capacity_ = 0;
|
||||
|
||||
// Live persisted value bytes. This excludes backend metadata and any
|
||||
// bucket files already unlinked from the cache namespace but still pending
|
||||
// async physical deletion.
|
||||
size_t capacity_ = 0;
|
||||
std::atomic<size_t> persisted_live_data_bytes_{0};
|
||||
};
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -3,12 +3,11 @@
|
|||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <variant>
|
||||
#include "Slab.h"
|
||||
#include "ylt/struct_json/json_reader.h"
|
||||
#include "ylt/struct_json/json_writer.h"
|
||||
|
|
@ -30,8 +29,9 @@ static constexpr uint64_t DEFAULT_KV_SOFT_PIN_TTL_MS =
|
|||
static constexpr bool DEFAULT_ALLOW_EVICT_SOFT_PINNED_OBJECTS = true;
|
||||
static constexpr double DEFAULT_EVICTION_RATIO = 0.05;
|
||||
static constexpr double DEFAULT_EVICTION_HIGH_WATERMARK_RATIO = 0.95;
|
||||
static constexpr int64_t ETCD_MASTER_VIEW_LEASE_TTL = 5; // in seconds
|
||||
static constexpr int64_t DEFAULT_CLIENT_LIVE_TTL_SEC = 10; // in seconds
|
||||
static constexpr int64_t ETCD_MASTER_VIEW_LEASE_TTL = 5; // in seconds
|
||||
static constexpr int64_t DEFAULT_CLIENT_LIVE_TTL_SEC = 10; // in seconds
|
||||
static constexpr int64_t DEFAULT_CLIENT_CRASHED_TTL_SEC = 30; // in seconds
|
||||
static const std::string DEFAULT_CLUSTER_ID = "mooncake_cluster";
|
||||
static const std::string DEFAULT_ROOT_FS_DIR = "";
|
||||
// default do not limit DFS usage, and use
|
||||
|
|
@ -90,8 +90,9 @@ UUID generate_uuid();
|
|||
* @brief Error codes for various operations in the system
|
||||
*/
|
||||
enum class ErrorCode : int32_t {
|
||||
OK = 0, ///< Operation successful.
|
||||
INTERNAL_ERROR = -1, ///< Internal error occurred.
|
||||
OK = 0, ///< Operation successful.
|
||||
INTERNAL_ERROR = -1, ///< Internal error occurred.
|
||||
NOT_IMPLEMENTED = -2, ///< Not implemented.
|
||||
|
||||
// Buffer allocation errors (Range: -20 to -99)
|
||||
BUFFER_OVERFLOW = -10, ///< Insufficient buffer space.
|
||||
|
|
@ -101,6 +102,9 @@ enum class ErrorCode : int32_t {
|
|||
SEGMENT_NOT_FOUND = -101, ///< No available segments found.
|
||||
SEGMENT_ALREADY_EXISTS = -102, ///< Segment already exists.
|
||||
CLIENT_NOT_FOUND = -103, ///< Client not found.
|
||||
CLIENT_ALREADY_EXISTS = -104, ///< Client already exists.
|
||||
CLIENT_UNHEALTHY =
|
||||
-105, ///< Client is not in a healthy state for the operation.
|
||||
|
||||
// Handle selection errors (Range: -200 to -299)
|
||||
NO_AVAILABLE_HANDLE =
|
||||
|
|
@ -108,6 +112,7 @@ enum class ErrorCode : int32_t {
|
|||
|
||||
// Version errors (Range: -300 to -399)
|
||||
INVALID_VERSION = -300, ///< Invalid version.
|
||||
CAS_FAILED = -301, ///< Compare and Swap failed (Optimistic Locking).
|
||||
|
||||
// Key errors (Range: -400 to -499)
|
||||
INVALID_KEY = -400, ///< Invalid key.
|
||||
|
|
@ -119,17 +124,19 @@ enum class ErrorCode : int32_t {
|
|||
INVALID_PARAMS = -600, ///< Invalid parameters.
|
||||
ILLEGAL_CLIENT = -601, ///< Illegal client to do the operation.
|
||||
|
||||
// Engine operation errors (Range: -700 to -799)
|
||||
// Engine operation errors (Range: -700 to -710)
|
||||
INVALID_WRITE = -700, ///< Invalid write operation.
|
||||
INVALID_READ = -701, ///< Invalid read operation.
|
||||
INVALID_REPLICA = -702, ///< Invalid replica operation.
|
||||
|
||||
// Object errors (Range: -703 to -707)
|
||||
REPLICA_IS_NOT_READY = -703, ///< Replica is not ready.
|
||||
OBJECT_NOT_FOUND = -704, ///< Object not found.
|
||||
OBJECT_ALREADY_EXISTS = -705, ///< Object already exists.
|
||||
OBJECT_HAS_LEASE = -706, ///< Object has lease.
|
||||
LEASE_EXPIRED = -707, ///< Lease expired before data transfer completed.
|
||||
REPLICA_ALREADY_EXISTS = -708, ///< Replica already exists.
|
||||
REPLICA_NOT_FOUND = -709, ///< Replica not found.
|
||||
REPLICA_NUM_EXCEEDED = -710, ///< Replica number exceeded.
|
||||
|
||||
// Transfer errors (Range: -800 to -899)
|
||||
TRANSFER_FAIL = -800, ///< Transfer operation failed.
|
||||
|
|
@ -162,6 +169,17 @@ enum class ErrorCode : int32_t {
|
|||
KEYS_ULTRA_LIMIT = -1203, ///< Keys ultra limit.
|
||||
UNABLE_OFFLOAD = -1300, ///< The offload functionality is not enabled
|
||||
UNABLE_OFFLOADING = -1301, ///< Unable offloading.
|
||||
|
||||
// Tiered backend errors (Range: -1400 to -1499)
|
||||
EMPTY_REPLICAS = -1400,
|
||||
TIER_NOT_FOUND = -1401,
|
||||
DATA_COPY_FAILED = -1402,
|
||||
|
||||
// Store errors (Range: -1500 to -1599)
|
||||
SHUTTING_DOWN = -1500, ///< Store is shutting down, rejecting new requests.
|
||||
ASYNC_ENQUEUE_FAILED = -1501, ///< Async metadata notifier enqueue failed
|
||||
///< (queue full/stopped).
|
||||
INACCESSIBLE_MASTER = -1502
|
||||
};
|
||||
|
||||
int32_t toInt(ErrorCode errorCode) noexcept;
|
||||
|
|
@ -174,6 +192,28 @@ inline std::ostream& operator<<(std::ostream& os,
|
|||
return os << toString(errorCode);
|
||||
}
|
||||
|
||||
enum class DeploymentMode {
|
||||
UNKNOWN = -1,
|
||||
CENTRALIZATION = 0,
|
||||
P2P,
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os,
|
||||
const DeploymentMode& mode) noexcept {
|
||||
switch (mode) {
|
||||
case DeploymentMode::CENTRALIZATION:
|
||||
os << "CENTRALIZATION";
|
||||
break;
|
||||
case DeploymentMode::P2P:
|
||||
os << "P2P";
|
||||
break;
|
||||
default:
|
||||
os << "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Represents a contiguous memory region
|
||||
*/
|
||||
|
|
@ -186,28 +226,105 @@ const static uint64_t kMinSliceSize = facebook::cachelib::Slab::kMinAllocSize;
|
|||
const static uint64_t kMaxSliceSize =
|
||||
facebook::cachelib::Slab::kSize - 16; // should be lower than limit
|
||||
|
||||
struct CentralizedSegmentExtraData {
|
||||
uintptr_t base{0};
|
||||
std::string te_endpoint;
|
||||
YLT_REFL(CentralizedSegmentExtraData, base, te_endpoint);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a contiguous memory region
|
||||
* @enum MemoryType
|
||||
* @brief Defines the physical storage medium type for a cache tier.
|
||||
*/
|
||||
enum class MemoryType { DRAM, NVME, ASCEND_NPU, UNKNOWN };
|
||||
|
||||
static inline std::string MemoryTypeToString(MemoryType type) {
|
||||
switch (type) {
|
||||
case MemoryType::DRAM:
|
||||
return "DRAM";
|
||||
case MemoryType::NVME:
|
||||
return "NVME";
|
||||
case MemoryType::ASCEND_NPU:
|
||||
return "ASCEND_NPU";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
struct P2PSegmentExtraData {
|
||||
int priority = 0;
|
||||
std::vector<std::string> tags;
|
||||
MemoryType memory_type = MemoryType::DRAM;
|
||||
size_t usage = 0;
|
||||
YLT_REFL(P2PSegmentExtraData, priority, tags, memory_type, usage);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a contiguous storage region
|
||||
*/
|
||||
struct Segment {
|
||||
UUID id{0, 0};
|
||||
std::string name{}; // Logical segment name used for preferred allocation
|
||||
uintptr_t base{0};
|
||||
size_t size{0};
|
||||
// TE p2p endpoint (ip:port) for transport-only addressing
|
||||
std::string te_endpoint{};
|
||||
Segment() = default;
|
||||
|
||||
// Polymorphic extra data
|
||||
std::variant<std::monostate, CentralizedSegmentExtraData,
|
||||
P2PSegmentExtraData>
|
||||
extra;
|
||||
|
||||
// Helper to check type
|
||||
bool IsP2PSegment() const {
|
||||
return std::holds_alternative<P2PSegmentExtraData>(extra);
|
||||
}
|
||||
|
||||
bool IsCentralizedSegment() const {
|
||||
return std::holds_alternative<CentralizedSegmentExtraData>(extra);
|
||||
}
|
||||
|
||||
bool IsEmpty() const {
|
||||
return std::holds_alternative<std::monostate>(extra);
|
||||
}
|
||||
|
||||
CentralizedSegmentExtraData& GetCentralizedExtra() {
|
||||
if (IsP2PSegment()) {
|
||||
throw std::runtime_error(
|
||||
"Segment already holds P2PSegmentExtraData; cannot assign "
|
||||
"CentralizedSegmentExtraData");
|
||||
}
|
||||
if (IsEmpty()) extra = CentralizedSegmentExtraData{};
|
||||
return std::get<CentralizedSegmentExtraData>(extra);
|
||||
}
|
||||
const CentralizedSegmentExtraData& GetCentralizedExtra() const {
|
||||
return std::get<CentralizedSegmentExtraData>(extra);
|
||||
}
|
||||
|
||||
P2PSegmentExtraData& GetP2PExtra() {
|
||||
if (IsCentralizedSegment()) {
|
||||
throw std::runtime_error(
|
||||
"Segment already holds CentralizedSegmentExtraData; cannot "
|
||||
"assign P2PSegmentExtraData");
|
||||
}
|
||||
if (IsEmpty()) extra = P2PSegmentExtraData{};
|
||||
return std::get<P2PSegmentExtraData>(extra);
|
||||
}
|
||||
const P2PSegmentExtraData& GetP2PExtra() const {
|
||||
return std::get<P2PSegmentExtraData>(extra);
|
||||
}
|
||||
};
|
||||
YLT_REFL(Segment, id, name, base, size, te_endpoint);
|
||||
YLT_REFL(Segment, id, name, size, extra);
|
||||
|
||||
/**
|
||||
* @brief Client status from the master's perspective
|
||||
* @brief Client status from the master's perspective.
|
||||
*
|
||||
* State machine: HEALTH -> DISCONNECTION (heartbeat timeout)
|
||||
* DISCONNECTION -> HEALTH (heartbeat recovered)
|
||||
* DISCONNECTION -> CRASHED (long-term timeout)
|
||||
*/
|
||||
enum class ClientStatus {
|
||||
UNDEFINED = 0, // Uninitialized
|
||||
OK, // Client is alive, no need to remount for now
|
||||
NEED_REMOUNT, // Ping ttl expired, or the first time connect to master,
|
||||
// so need to remount
|
||||
UNDEFINED = 0, // Client does not exist
|
||||
HEALTH, // Normal operation
|
||||
DISCONNECTION, // Heartbeat lost, waiting for recovery
|
||||
CRASHED, // Terminal state, all metadata will be cleaned up
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -217,14 +334,83 @@ inline std::ostream& operator<<(std::ostream& os,
|
|||
const ClientStatus& status) noexcept {
|
||||
static const std::unordered_map<ClientStatus, std::string_view>
|
||||
status_strings{{ClientStatus::UNDEFINED, "UNDEFINED"},
|
||||
{ClientStatus::OK, "OK"},
|
||||
{ClientStatus::NEED_REMOUNT, "NEED_REMOUNT"}};
|
||||
{ClientStatus::HEALTH, "HEALTH"},
|
||||
{ClientStatus::DISCONNECTION, "DISCONNECTION"},
|
||||
{ClientStatus::CRASHED, "CRASHED"}};
|
||||
|
||||
os << (status_strings.count(status) ? status_strings.at(status)
|
||||
: "UNKNOWN");
|
||||
return os;
|
||||
}
|
||||
|
||||
/**
|
||||
* @enum HAClientState
|
||||
* @brief Client-side HA state for Master crash recovery.
|
||||
* FULL: normal operation
|
||||
* DEGRADED: Master unreachable, local-only mode
|
||||
* SYNCING: re-syncing metadata to restarted Master
|
||||
*/
|
||||
enum class HAClientState : int32_t {
|
||||
FULL = 0,
|
||||
DEGRADED = 1,
|
||||
SYNCING = 2,
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os,
|
||||
const HAClientState& state) noexcept {
|
||||
switch (state) {
|
||||
case HAClientState::FULL:
|
||||
os << "FULL";
|
||||
break;
|
||||
case HAClientState::DEGRADED:
|
||||
os << "DEGRADED";
|
||||
break;
|
||||
case HAClientState::SYNCING:
|
||||
os << "SYNCING";
|
||||
break;
|
||||
default:
|
||||
os << "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
inline const char* toString(HAClientState state) noexcept {
|
||||
switch (state) {
|
||||
case HAClientState::FULL:
|
||||
return "FULL";
|
||||
case HAClientState::DEGRADED:
|
||||
return "DEGRADED";
|
||||
case HAClientState::SYNCING:
|
||||
return "SYNCING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @enum HAEvent
|
||||
* @brief Events that drive client-side HA state transitions.
|
||||
*/
|
||||
enum class HAEvent {
|
||||
MASTER_UNREACHABLE, // Consecutive heartbeat failures exceeded threshold
|
||||
MASTER_RECONNECTED, // Master connection restored. Triggered on:
|
||||
// 1. RegisterClient succeeded (Master restarted or
|
||||
// client re-registered after reconnection)
|
||||
// 2. Heartbeat recovered with HEALTH status after
|
||||
// a prior MASTER_UNREACHABLE event
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct ReplicaLocation
|
||||
* @brief Describes a single replica's key, tier and size.
|
||||
*/
|
||||
struct ReplicaLocation {
|
||||
std::string key;
|
||||
UUID tier_id;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
enum class BufferAllocatorType {
|
||||
CACHELIB = 0, // CachelibBufferAllocator
|
||||
OFFSET = 1, // OffsetBufferAllocator
|
||||
|
|
@ -253,4 +439,43 @@ struct StorageObjectMetadata {
|
|||
transport_endpoint);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief object iteration strategy in for-each interface
|
||||
*/
|
||||
enum class ObjectIterateStrategy {
|
||||
// Iterate over objects in order
|
||||
ORDERED = 0,
|
||||
|
||||
// Choose a random object
|
||||
RANDOM = 1,
|
||||
|
||||
// Choose a object with the most available capacity
|
||||
CAPACITY_PRIORITY = 2,
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(
|
||||
std::ostream& os, const ObjectIterateStrategy& strategy) noexcept {
|
||||
static const std::unordered_map<ObjectIterateStrategy, std::string_view>
|
||||
strategy_strings{
|
||||
{ObjectIterateStrategy::ORDERED, "ORDERED"},
|
||||
{ObjectIterateStrategy::RANDOM, "RANDOM"},
|
||||
{ObjectIterateStrategy::CAPACITY_PRIORITY, "CAPACITY_PRIORITY"},
|
||||
};
|
||||
|
||||
os << (strategy_strings.count(strategy) ? strategy_strings.at(strategy)
|
||||
: "UNKNOWN");
|
||||
return os;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<mooncake::UUID> {
|
||||
std::size_t operator()(const mooncake::UUID& k) const {
|
||||
std::size_t h1 = hash<uint64_t>{}(k.first);
|
||||
std::size_t h2 = hash<uint64_t>{}(k.second);
|
||||
return h1 ^ (h2 << 1);
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
|
|
|||
|
|
@ -2,23 +2,41 @@
|
|||
add_subdirectory(cachelib_memory_allocator)
|
||||
|
||||
set(MOONCAKE_STORE_SOURCES
|
||||
async_memcpy_executor.cpp
|
||||
replica.cpp
|
||||
allocator.cpp
|
||||
master_service.cpp
|
||||
centralized_master_service.cpp
|
||||
client_service.cpp
|
||||
centralized_client_service.cpp
|
||||
p2p_client_service.cpp
|
||||
client_metric.cpp
|
||||
types.cpp
|
||||
master_client.cpp
|
||||
centralized_master_client.cpp
|
||||
p2p_master_client.cpp
|
||||
utils.cpp
|
||||
master_metric_manager.cpp
|
||||
storage_backend.cpp
|
||||
thread_pool.cpp
|
||||
etcd_helper.cpp
|
||||
ha_helper.cpp
|
||||
segment.cpp
|
||||
centralized_segment_manager.cpp
|
||||
segment_manager.cpp
|
||||
client_meta.cpp
|
||||
client_manager.cpp
|
||||
centralized_client_manager.cpp
|
||||
centralized_client_meta.cpp
|
||||
p2p_client_manager.cpp
|
||||
p2p_master_service.cpp
|
||||
p2p_client_meta.cpp
|
||||
p2p_segment_manager.cpp
|
||||
transfer_task.cpp
|
||||
etcd_helper.cpp
|
||||
ha_helper.cpp
|
||||
rpc_service.cpp
|
||||
centralized_rpc_service.cpp
|
||||
p2p_rpc_service.cpp
|
||||
offset_allocator.cpp
|
||||
posix_file.cpp
|
||||
client_buffer.cpp
|
||||
|
|
@ -26,6 +44,20 @@ set(MOONCAKE_STORE_SOURCES
|
|||
dummy_client.cpp
|
||||
http_metadata_server.cpp
|
||||
file_storage.cpp
|
||||
tiered_cache/copier_registry.cpp
|
||||
tiered_cache/data_copier.cpp
|
||||
tiered_cache/tiered_backend.cpp
|
||||
data_manager.cpp
|
||||
route_cache.cpp
|
||||
async_metadata_notifier.cpp
|
||||
ha_recovery_manager.cpp
|
||||
client_rpc_service.cpp
|
||||
peer_client.cpp
|
||||
tiered_cache/tiers/dram_tier.cpp
|
||||
tiered_cache/tiers/storage_tier.cpp
|
||||
tiered_cache/scheduler/simple_policy.cpp
|
||||
tiered_cache/scheduler/lru_policy.cpp
|
||||
tiered_cache/scheduler/client_scheduler.cpp
|
||||
)
|
||||
|
||||
set(EXTRA_LIBS "")
|
||||
|
|
@ -40,6 +72,12 @@ if(USE_3FS)
|
|||
set(EXTRA_LIBS ${HF3FS_API_LIB})
|
||||
endif()
|
||||
|
||||
# Ascend Cache Tier support
|
||||
if(USE_ASCEND_CACHE_TIER)
|
||||
list(APPEND MOONCAKE_STORE_SOURCES tiered_cache/tiers/ascend_tier.cpp)
|
||||
message(STATUS "Ascend Cache Tier enabled")
|
||||
endif()
|
||||
|
||||
# The cache_allocator library
|
||||
include_directories(${Python3_INCLUDE_DIRS})
|
||||
add_library(mooncake_store ${MOONCAKE_STORE_SOURCES})
|
||||
|
|
@ -56,6 +94,35 @@ target_link_libraries(mooncake_store
|
|||
PRIVATE
|
||||
transfer_engine
|
||||
)
|
||||
|
||||
# Link Ascend ACL library if USE_ASCEND_CACHE_TIER is enabled
|
||||
if(USE_ASCEND_CACHE_TIER)
|
||||
# Try to find Ascend ACL library
|
||||
set(ASCEND_SEARCH_PATHS
|
||||
$ENV{ASCEND_HOME}/lib64
|
||||
/usr/local/Ascend/ascend-toolkit/latest/lib64
|
||||
/usr/local/Ascend/nnrt/latest/lib64
|
||||
)
|
||||
find_library(ASCENDCL_LIB ascendcl PATHS ${ASCEND_SEARCH_PATHS} NO_DEFAULT_PATH)
|
||||
|
||||
if(NOT ASCENDCL_LIB)
|
||||
message(WARNING "ascendcl library not found. Ascend cache tier will use fallback mode (no ACL).")
|
||||
else()
|
||||
message(STATUS "Found ascendcl library: ${ASCENDCL_LIB}")
|
||||
target_link_libraries(mooncake_store PUBLIC ${ASCENDCL_LIB})
|
||||
|
||||
# Add include paths for Ascend headers
|
||||
set(ASCEND_INCLUDE_PATHS
|
||||
$ENV{ASCEND_HOME}/include
|
||||
/usr/local/Ascend/ascend-toolkit/latest/include
|
||||
/usr/local/Ascend/nnrt/latest/include
|
||||
)
|
||||
target_include_directories(mooncake_store PUBLIC ${ASCEND_INCLUDE_PATHS})
|
||||
|
||||
# Only define USE_ASCEND_CACHE_TIER when library is found
|
||||
target_compile_definitions(mooncake_store PUBLIC USE_ASCEND_CACHE_TIER)
|
||||
endif()
|
||||
endif()
|
||||
if (STORE_USE_ETCD)
|
||||
add_dependencies(mooncake_store build_etcd_wrapper)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -56,12 +56,14 @@ std::ostream& operator<<(std::ostream& os, const AllocatedBuffer& buffer) {
|
|||
// Removed allocated_bytes parameter and member initialization
|
||||
CachelibBufferAllocator::CachelibBufferAllocator(std::string segment_name,
|
||||
size_t base, size_t size,
|
||||
std::string transport_endpoint)
|
||||
std::string transport_endpoint,
|
||||
const UUID& segment_id)
|
||||
: segment_name_(segment_name),
|
||||
base_(base),
|
||||
total_size_(size),
|
||||
cur_size_(0),
|
||||
transport_endpoint_(std::move(transport_endpoint)) {
|
||||
transport_endpoint_(std::move(transport_endpoint)),
|
||||
segment_id_(segment_id) {
|
||||
VLOG(1) << "initializing_buffer_allocator segment_name=" << segment_name
|
||||
<< " base_address=" << reinterpret_cast<void*>(base)
|
||||
<< " size=" << size;
|
||||
|
|
@ -121,7 +123,8 @@ std::unique_ptr<AllocatedBuffer> CachelibBufferAllocator::allocate(
|
|||
<< " segment=" << segment_name_ << " address=" << buffer;
|
||||
cur_size_.fetch_add(size);
|
||||
MasterMetricManager::instance().inc_allocated_mem_size(segment_name_, size);
|
||||
return std::make_unique<AllocatedBuffer>(shared_from_this(), buffer, size);
|
||||
return std::make_unique<AllocatedBuffer>(shared_from_this(), buffer, size,
|
||||
segment_id_);
|
||||
}
|
||||
|
||||
void CachelibBufferAllocator::deallocate(AllocatedBuffer* handle) {
|
||||
|
|
@ -145,12 +148,14 @@ void CachelibBufferAllocator::deallocate(AllocatedBuffer* handle) {
|
|||
// OffsetBufferAllocator implementation
|
||||
OffsetBufferAllocator::OffsetBufferAllocator(std::string segment_name,
|
||||
size_t base, size_t size,
|
||||
std::string transport_endpoint)
|
||||
std::string transport_endpoint,
|
||||
const UUID& segment_id)
|
||||
: segment_name_(segment_name),
|
||||
base_(base),
|
||||
total_size_(size),
|
||||
cur_size_(0),
|
||||
transport_endpoint_(std::move(transport_endpoint)) {
|
||||
transport_endpoint_(std::move(transport_endpoint)),
|
||||
segment_id_(segment_id) {
|
||||
VLOG(1) << "initializing_offset_buffer_allocator segment_name="
|
||||
<< segment_name << " base_address=" << reinterpret_cast<void*>(base)
|
||||
<< " size=" << size;
|
||||
|
|
@ -212,7 +217,8 @@ std::unique_ptr<AllocatedBuffer> OffsetBufferAllocator::allocate(size_t size) {
|
|||
// Create a custom AllocatedBuffer that manages the
|
||||
// OffsetAllocationHandle
|
||||
allocated_buffer = std::make_unique<AllocatedBuffer>(
|
||||
shared_from_this(), buffer_ptr, size, std::move(allocation_handle));
|
||||
shared_from_this(), buffer_ptr, size, segment_id_,
|
||||
std::move(allocation_handle));
|
||||
VLOG(1) << "allocation_succeeded size=" << size
|
||||
<< " segment=" << segment_name_ << " address=" << buffer_ptr;
|
||||
} catch (const std::exception& e) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
#include "async_memcpy_executor.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
// ============================================================================
|
||||
// ExecuteLocalCopyPlan
|
||||
// ============================================================================
|
||||
|
||||
ErrorCode ExecuteLocalCopyPlan(const LocalCopyPlan& plan) {
|
||||
if (plan.use_single_dest) {
|
||||
if (!plan.single_dest_ptr) {
|
||||
LOG(ERROR) << "Local copy destination buffer is null";
|
||||
return ErrorCode::INVALID_PARAMS;
|
||||
}
|
||||
if (plan.single_dest_size < plan.source_size) {
|
||||
LOG(ERROR) << "Local copy destination is too small, required="
|
||||
<< plan.source_size
|
||||
<< ", provided=" << plan.single_dest_size;
|
||||
return ErrorCode::INVALID_PARAMS;
|
||||
}
|
||||
if (plan.source_size > 0) {
|
||||
std::memcpy(plan.single_dest_ptr, plan.source_ptr,
|
||||
plan.source_size);
|
||||
}
|
||||
return ErrorCode::OK;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
for (const auto& slice : plan.dest_slices) {
|
||||
if (offset >= plan.source_size) break;
|
||||
const size_t copy_size =
|
||||
std::min(slice.size, plan.source_size - offset);
|
||||
if (copy_size == 0) continue;
|
||||
if (!slice.ptr) {
|
||||
LOG(ERROR) << "Local copy destination buffer is null";
|
||||
return ErrorCode::INVALID_PARAMS;
|
||||
}
|
||||
std::memcpy(slice.ptr, plan.source_ptr + offset, copy_size);
|
||||
offset += copy_size;
|
||||
}
|
||||
|
||||
if (offset != plan.source_size) {
|
||||
LOG(ERROR) << "Local copy did not complete, copied=" << offset
|
||||
<< ", source_size=" << plan.source_size;
|
||||
return ErrorCode::INTERNAL_ERROR;
|
||||
}
|
||||
return ErrorCode::OK;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AsyncMemcpyExecutor
|
||||
// ============================================================================
|
||||
|
||||
AsyncMemcpyExecutor::AsyncMemcpyExecutor(size_t worker_num) {
|
||||
workers_.reserve(std::max<size_t>(1, worker_num));
|
||||
for (size_t i = 0; i < std::max<size_t>(1, worker_num); ++i) {
|
||||
workers_.emplace_back(&AsyncMemcpyExecutor::WorkerMain, this);
|
||||
}
|
||||
}
|
||||
|
||||
AsyncMemcpyExecutor::~AsyncMemcpyExecutor() { Shutdown(); }
|
||||
|
||||
void AsyncMemcpyExecutor::Shutdown() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (shutting_down_) {
|
||||
return;
|
||||
}
|
||||
shutting_down_ = true;
|
||||
}
|
||||
queue_not_empty_cv_.notify_all();
|
||||
|
||||
for (auto& worker : workers_) {
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
workers_.clear();
|
||||
|
||||
std::queue<QueueTask> pending;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
pending.swap(tasks_);
|
||||
}
|
||||
while (!pending.empty()) {
|
||||
auto task = std::move(pending.front());
|
||||
if (task.cancel) {
|
||||
task.cancel();
|
||||
}
|
||||
pending.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncMemcpyExecutor::WorkerMain() {
|
||||
while (true) {
|
||||
QueueTask task;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
queue_not_empty_cv_.wait(
|
||||
lock, [this] { return shutting_down_ || !tasks_.empty(); });
|
||||
if (shutting_down_ && tasks_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
task = std::move(tasks_.front());
|
||||
tasks_.pop();
|
||||
}
|
||||
|
||||
try {
|
||||
if (task.run) {
|
||||
task.run();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG(ERROR) << "Async worker task threw exception: " << e.what();
|
||||
if (task.cancel) {
|
||||
task.cancel();
|
||||
}
|
||||
} catch (...) {
|
||||
LOG(ERROR) << "Async worker task threw unknown exception";
|
||||
if (task.cancel) {
|
||||
task.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,479 @@
|
|||
#include "async_metadata_notifier.h"
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
AsyncMetadataNotifier::AsyncMetadataNotifier(P2PMasterClient& master_client,
|
||||
const UUID& client_id,
|
||||
size_t sender_thread_count,
|
||||
size_t max_batch_size,
|
||||
size_t queue_capacity,
|
||||
SyncFailureCallback failure_cb)
|
||||
: master_client_(master_client),
|
||||
client_id_(client_id),
|
||||
sender_thread_count_(sender_thread_count),
|
||||
max_batch_size_(max_batch_size),
|
||||
batch_buffers_(sender_thread_count),
|
||||
failure_cb_(std::move(failure_cb)) {
|
||||
const size_t min_queue_capacity = max_batch_size_ * sender_thread_count_;
|
||||
const size_t total_cap = std::max(queue_capacity, min_queue_capacity);
|
||||
const size_t per_shard = total_cap / sender_thread_count_;
|
||||
|
||||
shards_.reserve(sender_thread_count_);
|
||||
for (size_t i = 0; i < sender_thread_count_; ++i) {
|
||||
auto shard = std::make_unique<SenderShard>();
|
||||
shard->capacity = per_shard;
|
||||
shard->normal_reserved = per_shard / 4;
|
||||
shard->slots.resize(per_shard);
|
||||
shard->free_stack.reserve(per_shard);
|
||||
for (size_t j = 0; j < per_shard; ++j) {
|
||||
shard->free_stack.push_back(j);
|
||||
}
|
||||
shards_.push_back(std::move(shard));
|
||||
batch_buffers_[i].resize(max_batch_size_);
|
||||
}
|
||||
}
|
||||
|
||||
AsyncMetadataNotifier::~AsyncMetadataNotifier() { Stop(); }
|
||||
|
||||
void AsyncMetadataNotifier::Start() {
|
||||
bool expected = false;
|
||||
if (!running_.compare_exchange_strong(expected, true)) {
|
||||
return; // already running
|
||||
}
|
||||
|
||||
for (auto& shard : shards_) {
|
||||
ResetShard(*shard);
|
||||
}
|
||||
|
||||
LOG(INFO) << "AsyncMetadataNotifier starting with " << sender_thread_count_
|
||||
<< " sender shards, max_batch_size=" << max_batch_size_;
|
||||
|
||||
for (size_t i = 0; i < sender_thread_count_; ++i) {
|
||||
shards_[i]->sender_thread = std::thread([this, i]() { SenderLoop(i); });
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncMetadataNotifier::Stop(bool drop_pending) {
|
||||
bool expected = true;
|
||||
if (!running_.compare_exchange_strong(expected, false)) {
|
||||
return; // not running
|
||||
}
|
||||
|
||||
if (drop_pending) {
|
||||
drop_on_stop_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Wake all sender threads (both shard CVs and stop CV for retry sleeps)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk{stop_mutex_};
|
||||
stop_cv_.notify_all();
|
||||
}
|
||||
for (auto& shard : shards_) {
|
||||
shard->sender_cv.notify_all();
|
||||
shard->producer_cv.notify_all();
|
||||
}
|
||||
for (auto& shard : shards_) {
|
||||
if (shard->sender_thread.joinable()) {
|
||||
shard->sender_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all queues so next Start() begins clean.
|
||||
for (auto& shard : shards_) {
|
||||
ResetShard(*shard);
|
||||
}
|
||||
drop_on_stop_.store(false, std::memory_order_release);
|
||||
consecutive_rpc_failures_.store(0, std::memory_order_release);
|
||||
|
||||
LOG(INFO) << "AsyncMetadataNotifier stopped";
|
||||
}
|
||||
|
||||
void AsyncMetadataNotifier::ResetShard(SenderShard& shard) {
|
||||
std::lock_guard<std::mutex> lock(shard.mutex);
|
||||
if (!shard.IsEmpty()) {
|
||||
LOG(WARNING) << "AsyncMetadataNotifier: discarding "
|
||||
<< shard.normal_count << " normal + "
|
||||
<< shard.recovery_count << " recovery pending ops";
|
||||
}
|
||||
shard.normal_head = shard.normal_tail = InvalidIdx;
|
||||
shard.normal_count = 0;
|
||||
shard.recovery_head = shard.recovery_tail = InvalidIdx;
|
||||
shard.recovery_count = 0;
|
||||
shard.recovery_in_flight = 0;
|
||||
shard.free_stack.clear();
|
||||
for (size_t i = 0; i < shard.capacity; ++i) {
|
||||
shard.slots[i] = Slot{};
|
||||
shard.free_stack.push_back(i);
|
||||
}
|
||||
shard.coalesce_index.clear();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Enqueue
|
||||
// ============================================================================
|
||||
|
||||
tl::expected<void, ErrorCode> AsyncMetadataNotifier::EnqueueAdd(
|
||||
const std::string& key, const UUID& segment_id, size_t size) {
|
||||
PendingOp op;
|
||||
op.type = PendingOp::ADD;
|
||||
op.key = key;
|
||||
op.segment_id = segment_id;
|
||||
op.size = size;
|
||||
return DoEnqueue(std::move(op), /*is_recovery=*/false);
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> AsyncMetadataNotifier::EnqueueRemove(
|
||||
const std::string& key, const UUID& segment_id) {
|
||||
PendingOp op;
|
||||
op.type = PendingOp::REMOVE;
|
||||
op.key = key;
|
||||
op.segment_id = segment_id;
|
||||
return DoEnqueue(std::move(op), /*is_recovery=*/false);
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> AsyncMetadataNotifier::EnqueueRecoveryAdd(
|
||||
const std::string& key, const UUID& segment_id, size_t size) {
|
||||
PendingOp op;
|
||||
op.type = PendingOp::ADD;
|
||||
op.key = key;
|
||||
op.segment_id = segment_id;
|
||||
op.size = size;
|
||||
return DoEnqueue(std::move(op), /*is_recovery=*/true);
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> AsyncMetadataNotifier::DoEnqueue(
|
||||
PendingOp&& op, bool is_recovery) {
|
||||
if (!running_.load(std::memory_order_acquire)) {
|
||||
LOG(WARNING)
|
||||
<< "AsyncMetadataNotifier has stopped, fail to enqueue key="
|
||||
<< op.key;
|
||||
return tl::unexpected(ErrorCode::ASYNC_ENQUEUE_FAILED);
|
||||
}
|
||||
|
||||
const size_t shard_idx =
|
||||
std::hash<std::string>{}(op.key) % sender_thread_count_;
|
||||
auto& shard = *shards_[shard_idx];
|
||||
|
||||
std::unique_lock<std::mutex> lock(shard.mutex);
|
||||
|
||||
// 1. Handle full pool
|
||||
if (is_recovery ? shard.IsFullForRecovery() : shard.IsFull()) {
|
||||
if (is_recovery) {
|
||||
// Recovery: non-blocking, caller retries with sleep
|
||||
return tl::unexpected(ErrorCode::ASYNC_ENQUEUE_FAILED);
|
||||
}
|
||||
// Normal: block until space available
|
||||
bool ok = shard.producer_cv.wait_for(lock, EnqueueTimeout, [&] {
|
||||
return !shard.IsFull() || !running_.load(std::memory_order_relaxed);
|
||||
});
|
||||
if (!ok || !running_.load(std::memory_order_relaxed)) {
|
||||
LOG(WARNING) << "AsyncMetadataNotifier: enqueue timeout/shutdown"
|
||||
<< ", key=" << op.key;
|
||||
return tl::unexpected(ErrorCode::ASYNC_ENQUEUE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Coalescing: check for existing pending op
|
||||
CoalesceKey ck{op.key, op.segment_id};
|
||||
auto [ci, inserted] = shard.coalesce_index.emplace(ck, CoalesceEntry{});
|
||||
auto& entry = ci->second;
|
||||
|
||||
// 2a. Duplicate same-type op already pending — treat as success, skip
|
||||
if (op.type == PendingOp::ADD && entry.add_idx != InvalidIdx) {
|
||||
return {};
|
||||
}
|
||||
if (op.type == PendingOp::REMOVE && entry.remove_idx != InvalidIdx) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 2b. Opposite-type op pending — cancel it
|
||||
if (op.type == PendingOp::ADD && entry.remove_idx != InvalidIdx) {
|
||||
// Cancel the pending REMOVE, don't insert this ADD
|
||||
size_t cancel_idx = entry.remove_idx;
|
||||
bool was_recovery = shard.slots[cancel_idx].is_recovery;
|
||||
shard.Unlink(cancel_idx);
|
||||
shard.FreeSlot(cancel_idx);
|
||||
entry.remove_idx = InvalidIdx;
|
||||
shard.coalesce_index.erase(ci);
|
||||
lock.unlock();
|
||||
shard.producer_cv.notify_one();
|
||||
if (was_recovery) shard.recovery_drain_cv.notify_all();
|
||||
return {};
|
||||
}
|
||||
if (op.type == PendingOp::REMOVE && entry.add_idx != InvalidIdx) {
|
||||
// Cancel the pending ADD, don't insert this REMOVE
|
||||
size_t cancel_idx = entry.add_idx;
|
||||
bool was_recovery = shard.slots[cancel_idx].is_recovery;
|
||||
shard.Unlink(cancel_idx);
|
||||
shard.FreeSlot(cancel_idx);
|
||||
entry.add_idx = InvalidIdx;
|
||||
shard.coalesce_index.erase(ci);
|
||||
lock.unlock();
|
||||
shard.producer_cv.notify_one();
|
||||
if (was_recovery) shard.recovery_drain_cv.notify_all();
|
||||
return {};
|
||||
}
|
||||
|
||||
// 3. No coalescing: alloc slot, write data, link to tail
|
||||
size_t idx = shard.AllocSlot();
|
||||
shard.slots[idx].op = std::move(op);
|
||||
shard.LinkTail(idx, is_recovery);
|
||||
|
||||
if (shard.slots[idx].op.type == PendingOp::ADD) {
|
||||
entry.add_idx = idx;
|
||||
} else {
|
||||
entry.remove_idx = idx;
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
shard.sender_cv.notify_one();
|
||||
return {};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sender
|
||||
// ============================================================================
|
||||
|
||||
void AsyncMetadataNotifier::SenderLoop(size_t shard_idx) {
|
||||
auto& shard = *shards_[shard_idx];
|
||||
auto& batch = batch_buffers_[shard_idx];
|
||||
|
||||
while (true) {
|
||||
if (running_.load(std::memory_order_acquire) && IsPaused()) {
|
||||
std::unique_lock<std::mutex> lock(shard.mutex);
|
||||
shard.sender_cv.wait_for(lock, CircuitBreakerCooldown, [&] {
|
||||
return !running_.load(std::memory_order_relaxed) || !IsPaused();
|
||||
});
|
||||
}
|
||||
|
||||
auto [n, recovery_n] = CollectBatch(shard, batch);
|
||||
|
||||
if (n == 0) {
|
||||
if (!running_.load(std::memory_order_acquire)) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
SendBatch(batch, n);
|
||||
|
||||
// Decrement recovery_in_flight after the batch is actually sent.
|
||||
// Notify WaitForRecoveryDrain when all recovery ops are drained.
|
||||
if (recovery_n > 0) {
|
||||
std::lock_guard<std::mutex> lk(shard.mutex);
|
||||
shard.recovery_in_flight -= recovery_n;
|
||||
if (shard.recovery_in_flight == 0 && shard.recovery_count == 0) {
|
||||
shard.recovery_drain_cv.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<size_t, size_t> AsyncMetadataNotifier::CollectBatch(
|
||||
SenderShard& shard, std::vector<PendingOp>& batch_out) {
|
||||
std::unique_lock<std::mutex> lock(shard.mutex);
|
||||
|
||||
shard.sender_cv.wait_for(lock, BatchTimeout, [&] {
|
||||
return !shard.IsEmpty() || !running_.load(std::memory_order_relaxed);
|
||||
});
|
||||
|
||||
// drop mode: don't collect any more batches — let the sender exit cleanly
|
||||
if (drop_on_stop_.load(std::memory_order_relaxed)) {
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
// Phase 1: collect from normal queue (high priority)
|
||||
size_t collected =
|
||||
CollectFromList(shard, batch_out, 0, max_batch_size_, false);
|
||||
// Phase 2: fill remaining from recovery queue (low priority)
|
||||
size_t remaining = max_batch_size_ - collected;
|
||||
size_t recovery_collected = 0;
|
||||
if (remaining > 0) {
|
||||
recovery_collected =
|
||||
CollectFromList(shard, batch_out, collected, remaining, true);
|
||||
collected += recovery_collected;
|
||||
}
|
||||
|
||||
if (collected > 0) {
|
||||
if (recovery_collected > 0) {
|
||||
shard.recovery_in_flight += recovery_collected;
|
||||
}
|
||||
lock.unlock();
|
||||
shard.producer_cv.notify_all();
|
||||
}
|
||||
|
||||
return {collected, recovery_collected};
|
||||
}
|
||||
|
||||
size_t AsyncMetadataNotifier::CollectFromList(SenderShard& shard,
|
||||
std::vector<PendingOp>& batch_out,
|
||||
size_t offset, size_t max_count,
|
||||
bool from_recovery) {
|
||||
auto& head = from_recovery ? shard.recovery_head : shard.normal_head;
|
||||
auto& count = from_recovery ? shard.recovery_count : shard.normal_count;
|
||||
|
||||
size_t collected = 0;
|
||||
while (count > 0 && collected < max_count) {
|
||||
size_t idx = head;
|
||||
auto& slot = shard.slots[idx];
|
||||
|
||||
// Remove from coalesce index
|
||||
CoalesceKey ck{slot.op.key, slot.op.segment_id};
|
||||
auto ci = shard.coalesce_index.find(ck);
|
||||
if (ci != shard.coalesce_index.end()) {
|
||||
if (slot.op.type == PendingOp::ADD) {
|
||||
ci->second.add_idx = InvalidIdx;
|
||||
} else {
|
||||
ci->second.remove_idx = InvalidIdx;
|
||||
}
|
||||
if (ci->second.add_idx == InvalidIdx &&
|
||||
ci->second.remove_idx == InvalidIdx) {
|
||||
shard.coalesce_index.erase(ci);
|
||||
}
|
||||
}
|
||||
|
||||
batch_out[offset + collected] = std::move(slot.op);
|
||||
shard.Unlink(idx);
|
||||
shard.FreeSlot(idx);
|
||||
collected++;
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
void AsyncMetadataNotifier::SendBatch(std::vector<PendingOp>& batch,
|
||||
size_t count) {
|
||||
BatchSyncReplicaRequest req;
|
||||
req.client_id = client_id_;
|
||||
|
||||
req.add_keys.reserve(count);
|
||||
req.add_sizes.reserve(count);
|
||||
req.add_segment_ids.reserve(count);
|
||||
req.remove_keys.reserve(count);
|
||||
req.remove_segment_ids.reserve(count);
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto& op = batch[i];
|
||||
if (op.type == PendingOp::ADD) {
|
||||
req.add_keys.push_back(std::move(op.key));
|
||||
req.add_sizes.push_back(op.size);
|
||||
req.add_segment_ids.push_back(op.segment_id);
|
||||
} else {
|
||||
req.remove_keys.push_back(std::move(op.key));
|
||||
req.remove_segment_ids.push_back(op.segment_id);
|
||||
}
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < MaxRetryCount; ++attempt) {
|
||||
auto result = master_client_.BatchSyncReplica(req);
|
||||
if (result.has_value()) {
|
||||
RecordSuccess();
|
||||
auto& resp = result.value();
|
||||
for (size_t i = 0; i < resp.add_results.size(); ++i) {
|
||||
auto ec = resp.add_results[i];
|
||||
if (ec != ErrorCode::OK &&
|
||||
ec != ErrorCode::REPLICA_ALREADY_EXISTS) {
|
||||
LOG(WARNING)
|
||||
<< "BatchSyncReplica ADD key=" << req.add_keys[i]
|
||||
<< " failed: " << toString(ec);
|
||||
if (failure_cb_) {
|
||||
failure_cb_(req.add_keys[i], req.add_segment_ids[i],
|
||||
ec);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < resp.remove_results.size(); ++i) {
|
||||
if (resp.remove_results[i] != ErrorCode::OK) {
|
||||
LOG(WARNING)
|
||||
<< "BatchSyncReplica REMOVE key=" << req.remove_keys[i]
|
||||
<< " failed: " << toString(resp.remove_results[i]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
LOG(WARNING) << "BatchSyncReplica attempt " << attempt
|
||||
<< " failed: " << toString(result.error());
|
||||
if (attempt < MaxRetryCount - 1) {
|
||||
// Use CV wait instead of sleep so Stop() can interrupt promptly
|
||||
auto backoff = std::chrono::milliseconds(100 * (1 << attempt));
|
||||
std::unique_lock<std::mutex> lk{stop_mutex_};
|
||||
if (stop_cv_.wait_for(lk, backoff, [&] {
|
||||
return !running_.load(std::memory_order_relaxed);
|
||||
})) {
|
||||
// Stop requested — abort retry, drop this batch
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
RecordFailure();
|
||||
LOG(ERROR) << "BatchSyncReplica failed after " << MaxRetryCount + 1
|
||||
<< " attempts, dropping " << count << " ops";
|
||||
// Rollback: notify failure for all ADD ops so local replicas get cleaned up
|
||||
if (failure_cb_) {
|
||||
for (size_t i = 0; i < req.add_keys.size(); ++i) {
|
||||
failure_cb_(req.add_keys[i], req.add_segment_ids[i],
|
||||
ErrorCode::INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncMetadataNotifier::RecordSuccess() {
|
||||
int32_t prev =
|
||||
consecutive_rpc_failures_.exchange(0, std::memory_order_acq_rel);
|
||||
if (prev < 0) {
|
||||
LOG(INFO) << "AsyncMetadataNotifier: circuit breaker closed";
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncMetadataNotifier::RecordFailure() {
|
||||
int32_t old_val = consecutive_rpc_failures_.load(std::memory_order_acquire);
|
||||
while (true) {
|
||||
if (old_val < 0) return; // already paused, nothing to do
|
||||
int32_t new_val = old_val + 1;
|
||||
if (new_val >= CircuitBreakerThreshold) {
|
||||
new_val = -new_val; // flip to paused
|
||||
}
|
||||
if (consecutive_rpc_failures_.compare_exchange_weak(
|
||||
old_val, new_val, std::memory_order_acq_rel,
|
||||
std::memory_order_acquire)) {
|
||||
if (new_val < 0) {
|
||||
LOG(ERROR)
|
||||
<< "AsyncMetadataNotifier: circuit breaker OPEN after "
|
||||
<< -new_val << " consecutive failures";
|
||||
}
|
||||
return;
|
||||
}
|
||||
// old_val updated by CAS, retry
|
||||
}
|
||||
}
|
||||
|
||||
bool AsyncMetadataNotifier::WaitForRecoveryDrain(
|
||||
const std::function<bool()>& abort_fn, std::chrono::milliseconds timeout) {
|
||||
auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
|
||||
// Wait for each shard's recovery queue to drain independently.
|
||||
for (size_t i = 0; i < sender_thread_count_; ++i) {
|
||||
auto& shard = *shards_[i];
|
||||
std::unique_lock<std::mutex> lock(shard.mutex);
|
||||
while (shard.recovery_count > 0 || shard.recovery_in_flight > 0) {
|
||||
if (abort_fn && abort_fn()) return false;
|
||||
|
||||
auto remaining = deadline - std::chrono::steady_clock::now();
|
||||
if (remaining <= std::chrono::milliseconds::zero()) {
|
||||
LOG(WARNING) << "WaitForRecoveryDrain timed out, shard=" << i
|
||||
<< ", remaining_count=" << shard.recovery_count;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto wait_time = std::min(
|
||||
remaining,
|
||||
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
|
||||
std::chrono::milliseconds(100)));
|
||||
shard.recovery_drain_cv.wait_for(lock, wait_time);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
#include "centralized_client_manager.h"
|
||||
#include "centralized_client_meta.h"
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
namespace mooncake {
|
||||
CentralizedClientManager::CentralizedClientManager(
|
||||
const int64_t client_live_ttl_sec, const int64_t client_crashed_ttl_sec,
|
||||
const BufferAllocatorType memory_allocator_type,
|
||||
const ViewVersionId view_version)
|
||||
: ClientManager(client_live_ttl_sec, client_crashed_ttl_sec, view_version),
|
||||
memory_allocator_type_(memory_allocator_type),
|
||||
allocation_strategy_(std::make_shared<RandomAllocationStrategy>()) {}
|
||||
|
||||
auto CentralizedClientManager::MountLocalDiskSegment(const UUID& client_id,
|
||||
bool enable_offloading)
|
||||
-> tl::expected<void, ErrorCode> {
|
||||
auto client_meta = GetClient(client_id);
|
||||
if (!client_meta) {
|
||||
LOG(WARNING) << "MountLocalDiskSegment: client not found, client_id="
|
||||
<< client_id;
|
||||
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
auto centralized_meta =
|
||||
std::static_pointer_cast<CentralizedClientMeta>(client_meta);
|
||||
if (!centralized_meta) {
|
||||
LOG(ERROR) << "MountLocalDiskSegment: client meta type mismatch";
|
||||
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
auto ret = centralized_meta->MountLocalDiskSegment(enable_offloading);
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "fail to mount local disk segment"
|
||||
<< ", client_id=" << client_id << ", ret=" << ret.error();
|
||||
return ret;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto CentralizedClientManager::OffloadObjectHeartbeat(const UUID& client_id,
|
||||
bool enable_offloading)
|
||||
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode> {
|
||||
auto client_meta = GetClient(client_id);
|
||||
if (!client_meta) {
|
||||
LOG(WARNING) << "OffloadObjectHeartbeat: client not found, client_id="
|
||||
<< client_id;
|
||||
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
auto centralized_meta =
|
||||
std::static_pointer_cast<CentralizedClientMeta>(client_meta);
|
||||
if (!centralized_meta) {
|
||||
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
auto ret = centralized_meta->OffloadObjectHeartbeat(enable_offloading);
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "fail to offload object heartbeat"
|
||||
<< ", client_id=" << client_id << ", ret=" << ret.error();
|
||||
return ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto CentralizedClientManager::PushOffloadingQueue(
|
||||
const std::string& key, const int64_t size, const std::string& segment_name)
|
||||
-> tl::expected<void, ErrorCode> {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
// Find which client owns this segment
|
||||
for (auto& [id, meta] : client_metas_) {
|
||||
// QuerySegments in ClientMeta checks whether segment exists
|
||||
auto query_res = meta->QuerySegments(segment_name);
|
||||
if (query_res.has_value()) {
|
||||
auto centralized_meta =
|
||||
std::static_pointer_cast<CentralizedClientMeta>(meta);
|
||||
if (centralized_meta) {
|
||||
auto ret = centralized_meta->PushOffloadingQueue(key, size,
|
||||
segment_name);
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "fail to push offloading queue"
|
||||
<< ", key=" << key << ", size=" << size
|
||||
<< ", segment_name=" << segment_name
|
||||
<< ", ret=" << ret.error();
|
||||
return ret;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG(ERROR) << "Segment not found for offloading: " << segment_name;
|
||||
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
std::shared_ptr<ClientMeta> CentralizedClientManager::CreateClientMeta(
|
||||
const RegisterClientRequest& req) {
|
||||
auto meta = std::make_shared<CentralizedClientMeta>(req.client_id,
|
||||
memory_allocator_type_);
|
||||
// Register allocator change callback to sync global_allocator_manager_
|
||||
auto seg_mgr = std::static_pointer_cast<CentralizedSegmentManager>(
|
||||
meta->GetSegmentManager());
|
||||
if (seg_mgr) {
|
||||
seg_mgr->SetAllocatorChangeCallback(
|
||||
[this](const std::string& segment_name,
|
||||
const std::shared_ptr<BufferAllocatorBase>& allocator,
|
||||
bool is_add) -> tl::expected<void, ErrorCode> {
|
||||
SharedMutexLocker lock(&global_allocator_mutex_);
|
||||
if (is_add) {
|
||||
global_allocator_manager_.addAllocator(segment_name,
|
||||
allocator);
|
||||
} else if (!global_allocator_manager_.removeAllocator(
|
||||
segment_name, allocator)) {
|
||||
LOG(WARNING)
|
||||
<< "Failed to remove allocator: " << segment_name;
|
||||
}
|
||||
return {};
|
||||
});
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
auto CentralizedClientManager::Allocate(
|
||||
const uint64_t slice_length, const size_t replica_num,
|
||||
const std::vector<std::string>& preferred_segments)
|
||||
-> tl::expected<std::vector<Replica>, ErrorCode> {
|
||||
SharedMutexLocker lock(&global_allocator_mutex_, shared_lock);
|
||||
// WARNING: This function must NOT acquire segment_mutex_ which is in the
|
||||
// segment manager of client_meta.
|
||||
// For example, in SegmentManager's MountSegment/UnmountSegment():
|
||||
// 1. SegmentManager acquires SegmentManager lock (L1) at first
|
||||
// 2. Then it calls the allocator change callback, which acquires the
|
||||
// GlobalAllocatorManager lock (L2)
|
||||
// 3. Then, in CentralizedClientManager::Allocate, it acquires L2 at first.
|
||||
// Thus, if it attempts to acquire L1 here, it would cause a deadlock
|
||||
// (L2 -> L1 vs L1 -> L2).
|
||||
auto result =
|
||||
allocation_strategy_->Allocate(global_allocator_manager_, slice_length,
|
||||
replica_num, preferred_segments);
|
||||
if (!result.has_value()) {
|
||||
LOG(WARNING) << "No available replicas"
|
||||
<< ", slice_length=" << slice_length
|
||||
<< ", replica_num=" << replica_num;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
HeartbeatTaskResult CentralizedClientManager::ProcessTask(
|
||||
const UUID& client_id, const HeartbeatTask& task) {
|
||||
HeartbeatTaskResult result;
|
||||
result.type = task.type_;
|
||||
result.error = ErrorCode::NOT_IMPLEMENTED;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
#include "centralized_client_meta.h"
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
namespace mooncake {
|
||||
CentralizedClientMeta::CentralizedClientMeta(const UUID& client_id,
|
||||
BufferAllocatorType allocator_type)
|
||||
: ClientMeta(client_id) {
|
||||
segment_manager_ =
|
||||
std::make_shared<CentralizedSegmentManager>(allocator_type);
|
||||
}
|
||||
|
||||
std::shared_ptr<SegmentManager> CentralizedClientMeta::GetSegmentManager() {
|
||||
return segment_manager_;
|
||||
}
|
||||
std::shared_ptr<CentralizedSegmentManager>
|
||||
CentralizedClientMeta::GetCentralizedSegmentManager() {
|
||||
return segment_manager_;
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode>
|
||||
CentralizedClientMeta::QueryIp(const UUID& client_id) {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return tl::make_unexpected(check_ret.error());
|
||||
}
|
||||
return segment_manager_->QueryIp();
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedClientMeta::MountLocalDiskSegment(
|
||||
bool enable_offloading) {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return check_ret;
|
||||
}
|
||||
return segment_manager_->MountLocalDiskSegment(enable_offloading);
|
||||
}
|
||||
|
||||
auto CentralizedClientMeta::OffloadObjectHeartbeat(bool enable_offloading)
|
||||
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode> {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return tl::make_unexpected(check_ret.error());
|
||||
}
|
||||
return segment_manager_->OffloadObjectHeartbeat(enable_offloading);
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedClientMeta::PushOffloadingQueue(
|
||||
const std::string& key, const int64_t size,
|
||||
const std::string& segment_name) {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return check_ret;
|
||||
}
|
||||
return segment_manager_->PushOffloadingQueue(key, size, segment_name);
|
||||
}
|
||||
|
||||
void CentralizedClientMeta::DoOnDisconnected() {
|
||||
auto ret = segment_manager_->SetGlobalVisibility(false);
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "Failed to hide allocators for client " << client_id_
|
||||
<< " error=" << ret.error();
|
||||
}
|
||||
}
|
||||
|
||||
void CentralizedClientMeta::DoOnRecovered() {
|
||||
auto ret = segment_manager_->SetGlobalVisibility(true);
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "Failed to show allocators for client " << client_id_
|
||||
<< " error=" << ret.error();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,233 @@
|
|||
#include "centralized_master_client.h"
|
||||
#include "centralized_rpc_service.h"
|
||||
#include "utils/scoped_vlog_timer.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::PutStart> {
|
||||
static constexpr const char* value = "PutStart";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::BatchPutStart> {
|
||||
static constexpr const char* value = "BatchPutStart";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::PutEnd> {
|
||||
static constexpr const char* value = "PutEnd";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::BatchPutEnd> {
|
||||
static constexpr const char* value = "BatchPutEnd";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::PutRevoke> {
|
||||
static constexpr const char* value = "PutRevoke";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::BatchPutRevoke> {
|
||||
static constexpr const char* value = "BatchPutRevoke";
|
||||
};
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::BatchReplicaClear> {
|
||||
static constexpr const char* value = "BatchReplicaClear";
|
||||
};
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::GetFsdir> {
|
||||
static constexpr const char* value = "GetFsdir";
|
||||
};
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::GetStorageConfig> {
|
||||
static constexpr const char* value = "GetStorageConfig";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::MountLocalDiskSegment> {
|
||||
static constexpr const char* value = "MountLocalDiskSegment";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::OffloadObjectHeartbeat> {
|
||||
static constexpr const char* value = "OffloadObjectHeartbeat";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedCentralizedMasterService::NotifyOffloadSuccess> {
|
||||
static constexpr const char* value = "NotifyOffloadSuccess";
|
||||
};
|
||||
|
||||
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
|
||||
CentralizedMasterClient::PutStart(const std::string& key,
|
||||
const std::vector<size_t>& slice_lengths,
|
||||
const ReplicateConfig& config) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::PutStart");
|
||||
timer.LogRequest("key=", key, ", slice_count=", slice_lengths.size());
|
||||
|
||||
uint64_t total_slice_length = 0;
|
||||
for (const auto& slice_length : slice_lengths) {
|
||||
total_slice_length += slice_length;
|
||||
}
|
||||
|
||||
auto result = invoke_rpc<&WrappedCentralizedMasterService::PutStart,
|
||||
std::vector<Replica::Descriptor>>(
|
||||
client_id_, key, total_slice_length, config);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
||||
CentralizedMasterClient::BatchPutStart(
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<uint64_t>>& batch_slice_lengths,
|
||||
const ReplicateConfig& config) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::BatchPutStart");
|
||||
timer.LogRequest("keys_count=", keys.size());
|
||||
|
||||
std::vector<uint64_t> total_slice_lengths;
|
||||
total_slice_lengths.reserve(batch_slice_lengths.size());
|
||||
for (const auto& slice_lengths : batch_slice_lengths) {
|
||||
uint64_t total_slice_length = 0;
|
||||
for (const auto& slice_length : slice_lengths) {
|
||||
total_slice_length += slice_length;
|
||||
}
|
||||
total_slice_lengths.emplace_back(total_slice_length);
|
||||
}
|
||||
|
||||
auto result =
|
||||
invoke_batch_rpc<&WrappedCentralizedMasterService::BatchPutStart,
|
||||
std::vector<Replica::Descriptor>>(
|
||||
keys.size(), client_id_, keys, total_slice_lengths, config);
|
||||
timer.LogResponse("result=", result.size(), " operations");
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedMasterClient::PutEnd(
|
||||
const std::string& key, ReplicaType replica_type) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::PutEnd");
|
||||
timer.LogRequest("key=", key);
|
||||
|
||||
auto result = invoke_rpc<&WrappedCentralizedMasterService::PutEnd, void>(
|
||||
client_id_, key, replica_type);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> CentralizedMasterClient::BatchPutEnd(
|
||||
const std::vector<std::string>& keys) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::BatchPutEnd");
|
||||
timer.LogRequest("keys_count=", keys.size());
|
||||
|
||||
auto result =
|
||||
invoke_batch_rpc<&WrappedCentralizedMasterService::BatchPutEnd, void>(
|
||||
keys.size(), client_id_, keys);
|
||||
timer.LogResponse("result=", result.size(), " operations");
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedMasterClient::PutRevoke(
|
||||
const std::string& key, ReplicaType replica_type) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::PutRevoke");
|
||||
timer.LogRequest("key=", key);
|
||||
|
||||
auto result = invoke_rpc<&WrappedCentralizedMasterService::PutRevoke, void>(
|
||||
client_id_, key, replica_type);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>>
|
||||
CentralizedMasterClient::BatchPutRevoke(const std::vector<std::string>& keys) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::BatchPutRevoke");
|
||||
timer.LogRequest("keys_count=", keys.size());
|
||||
|
||||
auto result =
|
||||
invoke_batch_rpc<&WrappedCentralizedMasterService::BatchPutRevoke,
|
||||
void>(keys.size(), client_id_, keys);
|
||||
timer.LogResponse("result=", result.size(), " operations");
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode>
|
||||
CentralizedMasterClient::BatchReplicaClear(
|
||||
const std::vector<std::string>& object_keys, const UUID& client_id,
|
||||
const std::string& segment_name) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::BatchReplicaClear");
|
||||
timer.LogRequest("object_keys_count=", object_keys.size(),
|
||||
", client_id=", client_id,
|
||||
", segment_name=", segment_name);
|
||||
auto result =
|
||||
invoke_rpc<&WrappedCentralizedMasterService::BatchReplicaClear,
|
||||
std::vector<std::string>>(object_keys, client_id,
|
||||
segment_name);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::string, ErrorCode> CentralizedMasterClient::GetFsdir() {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::GetFsdir");
|
||||
timer.LogRequest("action=get_fsdir");
|
||||
|
||||
auto result =
|
||||
invoke_rpc<&WrappedCentralizedMasterService::GetFsdir, std::string>();
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<GetStorageConfigResponse, ErrorCode>
|
||||
CentralizedMasterClient::GetStorageConfig() {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::GetStorageConfig");
|
||||
timer.LogRequest("action=get_storage_config");
|
||||
|
||||
auto result = invoke_rpc<&WrappedCentralizedMasterService::GetStorageConfig,
|
||||
GetStorageConfigResponse>();
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedMasterClient::MountLocalDiskSegment(
|
||||
const UUID& client_id, bool enable_offloading) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::MountLocalDiskSegment");
|
||||
timer.LogRequest("client_id=", client_id,
|
||||
", enable_offloading=", enable_offloading);
|
||||
|
||||
auto result =
|
||||
invoke_rpc<&WrappedCentralizedMasterService::MountLocalDiskSegment,
|
||||
void>(client_id, enable_offloading);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>
|
||||
CentralizedMasterClient::OffloadObjectHeartbeat(const UUID& client_id,
|
||||
bool enable_offloading) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::OffloadObjectHeartbeat");
|
||||
timer.LogRequest("client_id=", client_id,
|
||||
", enable_offloading=", enable_offloading);
|
||||
|
||||
auto result =
|
||||
invoke_rpc<&WrappedCentralizedMasterService::OffloadObjectHeartbeat,
|
||||
std::unordered_map<std::string, int64_t>>(client_id,
|
||||
enable_offloading);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedMasterClient::NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas) {
|
||||
ScopedVLogTimer timer(1, "CentralizedMasterClient::NotifyOffloadSuccess");
|
||||
timer.LogRequest("client_id=", client_id, ", keys_count=", keys.size(),
|
||||
", metadatas_count=", metadatas.size());
|
||||
|
||||
auto result =
|
||||
invoke_rpc<&WrappedCentralizedMasterService::NotifyOffloadSuccess,
|
||||
void>(client_id, keys, metadatas);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,436 @@
|
|||
#include "centralized_rpc_service.h"
|
||||
#include "rpc_helper.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
WrappedCentralizedMasterService::WrappedCentralizedMasterService(
|
||||
const WrappedMasterServiceConfig& config)
|
||||
: WrappedMasterService(config),
|
||||
master_service_(MasterServiceConfig(config)) {
|
||||
init_centralized_http_server();
|
||||
}
|
||||
|
||||
void WrappedCentralizedMasterService::init_centralized_http_server() {
|
||||
using namespace coro_http;
|
||||
|
||||
http_server_.set_http_handler<GET>(
|
||||
"/get_all_segments",
|
||||
[&](coro_http_request& req, coro_http_response& resp) {
|
||||
resp.add_header("Content-Type", "text/plain; version=0.0.4");
|
||||
auto result = master_service_.GetAllSegments();
|
||||
if (result) {
|
||||
std::string ss = "";
|
||||
auto segments = result.value();
|
||||
for (const auto& segment_name : segments) {
|
||||
ss += segment_name;
|
||||
ss += "\n";
|
||||
}
|
||||
resp.set_status_and_content(status_type::ok, std::move(ss));
|
||||
} else {
|
||||
resp.set_status_and_content(status_type::internal_server_error,
|
||||
"Failed to get all segments");
|
||||
}
|
||||
});
|
||||
|
||||
http_server_.set_http_handler<GET>(
|
||||
"/query_segment",
|
||||
[&](coro_http_request& req, coro_http_response& resp) {
|
||||
auto segment = req.get_query_value("segment");
|
||||
resp.add_header("Content-Type", "text/plain; version=0.0.4");
|
||||
auto result = master_service_.QuerySegments(std::string(segment));
|
||||
|
||||
if (result) {
|
||||
std::string ss = "";
|
||||
auto [used, capacity] = result.value();
|
||||
ss += segment;
|
||||
ss += "\n";
|
||||
ss += "Used(bytes): ";
|
||||
ss += std::to_string(used);
|
||||
ss += "\nCapacity(bytes) : ";
|
||||
ss += std::to_string(capacity);
|
||||
ss += "\n";
|
||||
resp.set_status_and_content(status_type::ok, std::move(ss));
|
||||
} else {
|
||||
resp.set_status_and_content(status_type::internal_server_error,
|
||||
"Failed to query segment");
|
||||
}
|
||||
});
|
||||
|
||||
http_server_.set_http_handler<GET>(
|
||||
"/query_key", [&](coro_http_request& req, coro_http_response& resp) {
|
||||
auto key = req.get_query_value("key");
|
||||
auto get_result = master_service_.GetReplicaList(
|
||||
std::string(key),
|
||||
GetReplicaListRequestConfig(
|
||||
GetReplicaListRequestConfig::RETURN_ALL_CANDIDATES));
|
||||
resp.add_header("Content-Type", "text/plain; version=0.0.4");
|
||||
if (get_result) {
|
||||
std::string ss = "";
|
||||
const std::vector<Replica::Descriptor>& replicas =
|
||||
get_result.value().replicas;
|
||||
for (size_t i = 0; i < replicas.size(); i++) {
|
||||
if (replicas[i].is_memory_replica()) {
|
||||
auto& memory_descriptors =
|
||||
replicas[i].get_memory_descriptor();
|
||||
std::string tmp = "";
|
||||
struct_json::to_json(
|
||||
memory_descriptors.buffer_descriptor, tmp);
|
||||
ss += tmp;
|
||||
ss += "\n";
|
||||
}
|
||||
}
|
||||
resp.set_status_and_content(status_type::ok, std::move(ss));
|
||||
} else {
|
||||
resp.set_status_and_content(status_type::not_found,
|
||||
toString(get_result.error()));
|
||||
}
|
||||
});
|
||||
LOG(INFO) << "Centralized HTTP handlers initialized";
|
||||
}
|
||||
|
||||
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
|
||||
WrappedCentralizedMasterService::PutStart(const UUID& client_id,
|
||||
const std::string& key,
|
||||
const uint64_t slice_length,
|
||||
const ReplicateConfig& config) {
|
||||
return execute_rpc(
|
||||
"PutStart",
|
||||
[&] {
|
||||
return master_service_.PutStart(client_id, key, slice_length,
|
||||
config);
|
||||
},
|
||||
[&](auto& timer) {
|
||||
timer.LogRequest("client_id=", client_id, ", key=", key,
|
||||
", slice_length=", slice_length);
|
||||
},
|
||||
[&] { MasterMetricManager::instance().inc_put_start_requests(); },
|
||||
[] { MasterMetricManager::instance().inc_put_start_failures(); });
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> WrappedCentralizedMasterService::PutEnd(
|
||||
const UUID& client_id, const std::string& key, ReplicaType replica_type) {
|
||||
return execute_rpc(
|
||||
"PutEnd",
|
||||
[&] { return master_service_.PutEnd(client_id, key, replica_type); },
|
||||
[&](auto& timer) {
|
||||
timer.LogRequest("client_id=", client_id, ", key=", key,
|
||||
", replica_type=", replica_type);
|
||||
},
|
||||
[] { MasterMetricManager::instance().inc_put_end_requests(); },
|
||||
[] { MasterMetricManager::instance().inc_put_end_failures(); });
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> WrappedCentralizedMasterService::PutRevoke(
|
||||
const UUID& client_id, const std::string& key, ReplicaType replica_type) {
|
||||
return execute_rpc(
|
||||
"PutRevoke",
|
||||
[&] { return master_service_.PutRevoke(client_id, key, replica_type); },
|
||||
[&](auto& timer) {
|
||||
timer.LogRequest("client_id=", client_id, ", key=", key,
|
||||
", replica_type=", replica_type);
|
||||
},
|
||||
[] { MasterMetricManager::instance().inc_put_revoke_requests(); },
|
||||
[] { MasterMetricManager::instance().inc_put_revoke_failures(); });
|
||||
}
|
||||
|
||||
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
||||
WrappedCentralizedMasterService::BatchPutStart(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<uint64_t>& slice_lengths, const ReplicateConfig& config) {
|
||||
ScopedVLogTimer timer(1, "BatchPutStart");
|
||||
const size_t total_keys = keys.size();
|
||||
timer.LogRequest("client_id=", client_id, ", keys_count=", total_keys);
|
||||
MasterMetricManager::instance().inc_batch_put_start_requests(total_keys);
|
||||
|
||||
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
||||
results;
|
||||
results.reserve(keys.size());
|
||||
|
||||
if (config.prefer_alloc_in_same_node) {
|
||||
ReplicateConfig new_config = config;
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
auto result = master_service_.PutStart(
|
||||
client_id, keys[i], slice_lengths[i], new_config);
|
||||
results.emplace_back(result);
|
||||
if ((i == 0) && result.has_value()) {
|
||||
std::string preferred_segment;
|
||||
for (const auto& replica : result.value()) {
|
||||
if (replica.is_memory_replica()) {
|
||||
auto handles =
|
||||
replica.get_memory_descriptor().buffer_descriptor;
|
||||
if (!handles.transport_endpoint_.empty()) {
|
||||
preferred_segment = handles.transport_endpoint_;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!preferred_segment.empty()) {
|
||||
new_config.preferred_segment = preferred_segment;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
results.emplace_back(master_service_.PutStart(
|
||||
client_id, keys[i], slice_lengths[i], config));
|
||||
}
|
||||
}
|
||||
|
||||
size_t failure_count = 0;
|
||||
int no_available_handle_count = 0;
|
||||
for (size_t i = 0; i < results.size(); ++i) {
|
||||
if (!results[i].has_value()) {
|
||||
failure_count++;
|
||||
auto error = results[i].error();
|
||||
if (error == ErrorCode::OBJECT_ALREADY_EXISTS) {
|
||||
VLOG(1) << "BatchPutStart failed for key[" << i << "] '"
|
||||
<< keys[i] << "': " << toString(error);
|
||||
} else if (error == ErrorCode::NO_AVAILABLE_HANDLE) {
|
||||
no_available_handle_count++;
|
||||
} else {
|
||||
LOG(ERROR) << "BatchPutStart failed for key[" << i << "] '"
|
||||
<< keys[i] << "': " << toString(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (no_available_handle_count > 0) {
|
||||
LOG(WARNING) << "BatchPutStart failed for " << no_available_handle_count
|
||||
<< " keys" << PUT_NO_SPACE_HELPER_STR;
|
||||
}
|
||||
|
||||
if (failure_count == total_keys) {
|
||||
MasterMetricManager::instance().inc_batch_put_start_failures(
|
||||
failure_count);
|
||||
} else if (failure_count != 0) {
|
||||
MasterMetricManager::instance().inc_batch_put_start_partial_success(
|
||||
failure_count);
|
||||
}
|
||||
|
||||
timer.LogResponse("total=", results.size(),
|
||||
", success=", results.size() - failure_count,
|
||||
", failures=", failure_count);
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>>
|
||||
WrappedCentralizedMasterService::BatchPutEnd(
|
||||
const UUID& client_id, const std::vector<std::string>& keys) {
|
||||
ScopedVLogTimer timer(1, "BatchPutEnd");
|
||||
const size_t total_keys = keys.size();
|
||||
timer.LogRequest("client_id=", client_id, ", keys_count=", total_keys);
|
||||
MasterMetricManager::instance().inc_batch_put_end_requests(total_keys);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> results;
|
||||
results.reserve(keys.size());
|
||||
|
||||
for (const auto& key : keys) {
|
||||
results.emplace_back(
|
||||
master_service_.PutEnd(client_id, key, ReplicaType::MEMORY));
|
||||
}
|
||||
|
||||
size_t failure_count = 0;
|
||||
for (size_t i = 0; i < results.size(); ++i) {
|
||||
if (!results[i].has_value()) {
|
||||
failure_count++;
|
||||
auto error = results[i].error();
|
||||
LOG(ERROR) << "BatchPutEnd failed for key[" << i << "] '" << keys[i]
|
||||
<< "': " << toString(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (failure_count == total_keys) {
|
||||
MasterMetricManager::instance().inc_batch_put_end_failures(
|
||||
failure_count);
|
||||
} else if (failure_count != 0) {
|
||||
MasterMetricManager::instance().inc_batch_put_end_partial_success(
|
||||
failure_count);
|
||||
}
|
||||
|
||||
timer.LogResponse("total=", results.size(),
|
||||
", success=", results.size() - failure_count,
|
||||
", failures=", failure_count);
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>>
|
||||
WrappedCentralizedMasterService::BatchPutRevoke(
|
||||
const UUID& client_id, const std::vector<std::string>& keys) {
|
||||
ScopedVLogTimer timer(1, "BatchPutRevoke");
|
||||
const size_t total_keys = keys.size();
|
||||
timer.LogRequest("client_id=", client_id, ", keys_count=", total_keys);
|
||||
MasterMetricManager::instance().inc_batch_put_revoke_requests(total_keys);
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> results;
|
||||
results.reserve(keys.size());
|
||||
|
||||
for (const auto& key : keys) {
|
||||
results.emplace_back(
|
||||
master_service_.PutRevoke(client_id, key, ReplicaType::MEMORY));
|
||||
}
|
||||
|
||||
size_t failure_count = 0;
|
||||
for (size_t i = 0; i < results.size(); ++i) {
|
||||
if (!results[i].has_value()) {
|
||||
failure_count++;
|
||||
auto error = results[i].error();
|
||||
LOG(ERROR) << "BatchPutRevoke failed for key[" << i << "] '"
|
||||
<< keys[i] << "': " << toString(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (failure_count == total_keys) {
|
||||
MasterMetricManager::instance().inc_batch_put_revoke_failures(
|
||||
failure_count);
|
||||
} else if (failure_count != 0) {
|
||||
MasterMetricManager::instance().inc_batch_put_revoke_partial_success(
|
||||
failure_count);
|
||||
}
|
||||
|
||||
timer.LogResponse("total=", results.size(),
|
||||
", success=", results.size() - failure_count,
|
||||
", failures=", failure_count);
|
||||
return results;
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode>
|
||||
WrappedCentralizedMasterService::BatchReplicaClear(
|
||||
const std::vector<std::string>& object_keys, const UUID& client_id,
|
||||
const std::string& segment_name) {
|
||||
ScopedVLogTimer timer(1, "BatchReplicaClear");
|
||||
const size_t total_keys = object_keys.size();
|
||||
timer.LogRequest("object_keys_count=", total_keys,
|
||||
", client_id=", client_id,
|
||||
", segment_name=", segment_name);
|
||||
MasterMetricManager::instance().inc_batch_replica_clear_requests(
|
||||
total_keys);
|
||||
|
||||
auto result =
|
||||
master_service_.BatchReplicaClear(object_keys, client_id, segment_name);
|
||||
|
||||
size_t failure_count = 0;
|
||||
if (!result.has_value()) {
|
||||
failure_count = total_keys;
|
||||
LOG(WARNING) << "BatchReplicaClear failed: "
|
||||
<< toString(result.error());
|
||||
} else {
|
||||
const size_t cleared_count = result.value().size();
|
||||
failure_count = total_keys - cleared_count;
|
||||
timer.LogResponse("total=", total_keys, ", cleared=", cleared_count,
|
||||
", failed=", failure_count);
|
||||
}
|
||||
|
||||
if (failure_count == total_keys) {
|
||||
MasterMetricManager::instance().inc_batch_replica_clear_failures(
|
||||
failure_count);
|
||||
} else if (failure_count != 0) {
|
||||
MasterMetricManager::instance().inc_batch_replica_clear_partial_success(
|
||||
failure_count);
|
||||
}
|
||||
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::string, ErrorCode>
|
||||
WrappedCentralizedMasterService::GetFsdir() {
|
||||
ScopedVLogTimer timer(1, "GetFsdir");
|
||||
timer.LogRequest("action=get_fsdir");
|
||||
|
||||
auto result = master_service_.GetFsdir();
|
||||
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<GetStorageConfigResponse, ErrorCode>
|
||||
WrappedCentralizedMasterService::GetStorageConfig() {
|
||||
ScopedVLogTimer timer(1, "GetStorageConfig");
|
||||
timer.LogRequest("action=get_storage_config");
|
||||
|
||||
auto result = master_service_.GetStorageConfig();
|
||||
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode>
|
||||
WrappedCentralizedMasterService::MountLocalDiskSegment(const UUID& client_id,
|
||||
bool enable_offloading) {
|
||||
ScopedVLogTimer timer(1, "MountLocalDiskSegment");
|
||||
timer.LogRequest("action=mount_local_disk_segment");
|
||||
LOG(INFO) << "Mount local disk segment with client id is : " << client_id
|
||||
<< ", enable offloading is: " << enable_offloading;
|
||||
auto result =
|
||||
master_service_.MountLocalDiskSegment(client_id, enable_offloading);
|
||||
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::unordered_map<std::string, int64_t, std::hash<std::string>>,
|
||||
ErrorCode>
|
||||
WrappedCentralizedMasterService::OffloadObjectHeartbeat(
|
||||
const UUID& client_id, bool enable_offloading) {
|
||||
ScopedVLogTimer timer(1, "OffloadObjectHeartbeat");
|
||||
timer.LogRequest("action=offload_object_heartbeat");
|
||||
auto result =
|
||||
master_service_.OffloadObjectHeartbeat(client_id, enable_offloading);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode>
|
||||
WrappedCentralizedMasterService::NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas) {
|
||||
ScopedVLogTimer timer(1, "NotifyOffloadSuccess");
|
||||
timer.LogRequest("action=notify_offload_success");
|
||||
|
||||
auto result =
|
||||
master_service_.NotifyOffloadSuccess(client_id, keys, metadatas);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RegisterCentralizedRpcService(
|
||||
coro_rpc::coro_rpc_server& server,
|
||||
mooncake::WrappedCentralizedMasterService& wrapped_master_service) {
|
||||
RegisterRpcService(server, wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::BatchReplicaClear>(
|
||||
&wrapped_master_service);
|
||||
server
|
||||
.register_handler<&mooncake::WrappedCentralizedMasterService::PutStart>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<&mooncake::WrappedCentralizedMasterService::PutEnd>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::PutRevoke>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::BatchPutStart>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::BatchPutEnd>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::BatchPutRevoke>(
|
||||
&wrapped_master_service);
|
||||
|
||||
server
|
||||
.register_handler<&mooncake::WrappedCentralizedMasterService::GetFsdir>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::GetStorageConfig>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::MountLocalDiskSegment>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::OffloadObjectHeartbeat>(
|
||||
&wrapped_master_service);
|
||||
server.register_handler<
|
||||
&mooncake::WrappedCentralizedMasterService::NotifyOffloadSuccess>(
|
||||
&wrapped_master_service);
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
#include "centralized_segment_manager.h"
|
||||
#include <unordered_set>
|
||||
#include <glog/logging.h>
|
||||
|
||||
namespace mooncake {
|
||||
void CentralizedSegmentManager::SetAllocatorChangeCallback(
|
||||
AllocatorChangeCallback cb) {
|
||||
allocator_change_cb_ = std::move(cb);
|
||||
}
|
||||
tl::expected<void, ErrorCode> CentralizedSegmentManager::SetGlobalVisibility(
|
||||
bool visible) {
|
||||
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
|
||||
if (!allocator_change_cb_) {
|
||||
LOG(ERROR) << "allocator_change_cb_ is null";
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
|
||||
ErrorCode first_error = ErrorCode::OK;
|
||||
for (const auto& entry : mounted_segments_) {
|
||||
auto mounted_seg =
|
||||
std::static_pointer_cast<MountedCentralizedSegment>(entry.second);
|
||||
if (mounted_seg->buf_allocator) {
|
||||
auto ret = allocator_change_cb_(
|
||||
mounted_seg->name, mounted_seg->buf_allocator, visible);
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "SetGlobalVisibility failed for segment="
|
||||
<< mounted_seg->name << " visible=" << visible
|
||||
<< " error=" << ret.error();
|
||||
if (first_error == ErrorCode::OK) {
|
||||
first_error = ret.error();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first_error != ErrorCode::OK) {
|
||||
return tl::make_unexpected(first_error);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorCode CentralizedSegmentManager::InnerCheckMountSegment(
|
||||
const Segment& segment) {
|
||||
if (!segment.IsCentralizedSegment()) {
|
||||
LOG(ERROR) << "segment is not centralized";
|
||||
return ErrorCode::INVALID_PARAMS;
|
||||
}
|
||||
const uintptr_t buffer = segment.GetCentralizedExtra().base;
|
||||
const size_t size = segment.size;
|
||||
|
||||
// Check if parameters are valid before allocating memory.
|
||||
if (buffer == 0 || size == 0) {
|
||||
LOG(ERROR) << "buffer=" << buffer << " or size=" << size
|
||||
<< " is invalid";
|
||||
return ErrorCode::INVALID_PARAMS;
|
||||
}
|
||||
|
||||
if (memory_allocator_ == BufferAllocatorType::CACHELIB &&
|
||||
(buffer % facebook::cachelib::Slab::kSize ||
|
||||
size % facebook::cachelib::Slab::kSize)) {
|
||||
LOG(ERROR) << "buffer=" << buffer << " or size=" << size
|
||||
<< " is not aligned to " << facebook::cachelib::Slab::kSize
|
||||
<< " as required by Cachelib";
|
||||
return ErrorCode::INVALID_PARAMS;
|
||||
}
|
||||
|
||||
// Check if segment already exists
|
||||
auto exist_segment_it = mounted_segments_.find(segment.id);
|
||||
if (exist_segment_it != mounted_segments_.end()) {
|
||||
LOG(WARNING) << "segment_name=" << segment.name
|
||||
<< ", warn=segment_already_exists";
|
||||
return ErrorCode::SEGMENT_ALREADY_EXISTS;
|
||||
}
|
||||
|
||||
return ErrorCode::OK;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedSegmentManager::InnerMountSegment(
|
||||
const Segment& segment) {
|
||||
ErrorCode ret = InnerCheckMountSegment(segment);
|
||||
if (ret != ErrorCode::OK) {
|
||||
LOG(ERROR) << "fail to inner check mount segment"
|
||||
<< ", segment_name=" << segment.name << ", ret=" << ret;
|
||||
return tl::make_unexpected(ret);
|
||||
}
|
||||
const uintptr_t buffer = segment.GetCentralizedExtra().base;
|
||||
const size_t size = segment.size;
|
||||
std::shared_ptr<BufferAllocatorBase> allocator;
|
||||
// CachelibBufferAllocator may throw an exception if the size or base is
|
||||
// invalid for the slab allocator.
|
||||
try {
|
||||
// Create allocator based on the configured type
|
||||
switch (memory_allocator_) {
|
||||
case BufferAllocatorType::CACHELIB:
|
||||
allocator = std::make_shared<CachelibBufferAllocator>(
|
||||
segment.name, buffer, size,
|
||||
segment.GetCentralizedExtra().te_endpoint, segment.id);
|
||||
break;
|
||||
case BufferAllocatorType::OFFSET:
|
||||
allocator = std::make_shared<OffsetBufferAllocator>(
|
||||
segment.name, buffer, size,
|
||||
segment.GetCentralizedExtra().te_endpoint, segment.id);
|
||||
break;
|
||||
default:
|
||||
LOG(ERROR) << "segment_name=" << segment.name
|
||||
<< ", error=unknown_memory_allocator="
|
||||
<< static_cast<int>(memory_allocator_);
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
|
||||
if (!allocator) {
|
||||
LOG(ERROR) << "segment_name=" << segment.name
|
||||
<< ", error=failed_to_create_allocator";
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
} catch (...) {
|
||||
LOG(ERROR) << "segment_name=" << segment.name
|
||||
<< ", error=exception_during_allocator_creation";
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
|
||||
auto mounted_segment = std::make_shared<MountedCentralizedSegment>();
|
||||
static_cast<Segment&>(*mounted_segment) = segment;
|
||||
mounted_segment->buf_allocator = allocator;
|
||||
|
||||
if (allocator_change_cb_) {
|
||||
// Callback to add the allocator to the global manager
|
||||
auto ret = allocator_change_cb_(mounted_segment->name,
|
||||
mounted_segment->buf_allocator, true);
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "Failed to add allocator to global manager. "
|
||||
"Rolling back mount operation. "
|
||||
<< "segment=" << mounted_segment->name
|
||||
<< " error=" << ret.error();
|
||||
return tl::make_unexpected(ret.error());
|
||||
}
|
||||
}
|
||||
mounted_segments_[mounted_segment->id] = mounted_segment;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedSegmentManager::MountLocalDiskSegment(
|
||||
bool enable_offloading) {
|
||||
SharedMutexLocker lock_(&segment_mutex_);
|
||||
if (local_disk_segment_) {
|
||||
LOG(WARNING) << "warn=local_disk_segment_already_exists";
|
||||
return tl::make_unexpected(ErrorCode::SEGMENT_ALREADY_EXISTS);
|
||||
}
|
||||
local_disk_segment_ = std::make_shared<LocalDiskSegment>(enable_offloading);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto CentralizedSegmentManager::OffloadObjectHeartbeat(bool enable_offloading)
|
||||
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode> {
|
||||
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
|
||||
if (!local_disk_segment_) {
|
||||
LOG(ERROR) << "Local disk segment not found";
|
||||
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
|
||||
}
|
||||
MutexLocker locker(&local_disk_segment_->offloading_mutex_);
|
||||
local_disk_segment_->enable_offloading = enable_offloading;
|
||||
if (enable_offloading) {
|
||||
return std::move(local_disk_segment_->offloading_objects);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> CentralizedSegmentManager::PushOffloadingQueue(
|
||||
const std::string& key, const int64_t size,
|
||||
const std::string& segment_name) {
|
||||
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
|
||||
if (!local_disk_segment_) {
|
||||
LOG(ERROR) << "Local disk segment not found";
|
||||
return tl::make_unexpected(ErrorCode::UNABLE_OFFLOADING);
|
||||
}
|
||||
|
||||
// Check whether segment belongs to this client
|
||||
bool found = false;
|
||||
for (const auto& entry : mounted_segments_) {
|
||||
if (entry.second->name == segment_name) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LOG(ERROR) << "Segment " << segment_name << " not found";
|
||||
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
MutexLocker locker(&local_disk_segment_->offloading_mutex_);
|
||||
if (!local_disk_segment_->enable_offloading) {
|
||||
LOG(ERROR) << "Offloading is not enabled";
|
||||
return tl::make_unexpected(ErrorCode::UNABLE_OFFLOADING);
|
||||
}
|
||||
if (local_disk_segment_->offloading_objects.size() >=
|
||||
OFFLOADING_QUEUE_LIMIT) {
|
||||
LOG(ERROR) << "Offloading queue is full";
|
||||
return tl::make_unexpected(ErrorCode::KEYS_ULTRA_LIMIT);
|
||||
}
|
||||
local_disk_segment_->offloading_objects.emplace(key, size);
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode>
|
||||
CentralizedSegmentManager::QueryIp() {
|
||||
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
|
||||
|
||||
std::unordered_set<std::string> unique_ips;
|
||||
// Iterate mounted segments for this client
|
||||
for (const auto& entry : mounted_segments_) {
|
||||
const auto& segment = entry.second;
|
||||
if (!segment) {
|
||||
LOG(ERROR) << "unexpected null segment";
|
||||
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
||||
} else if (!segment->IsCentralizedSegment()) {
|
||||
LOG(ERROR) << "unexpected segment type"
|
||||
<< ", segment_id=" << segment->id
|
||||
<< ", segment_name=" << segment->name;
|
||||
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
const auto& extra = segment->GetCentralizedExtra();
|
||||
if (!extra.te_endpoint.empty()) {
|
||||
size_t colon_pos = extra.te_endpoint.find(':');
|
||||
if (colon_pos != std::string::npos) {
|
||||
std::string ip = extra.te_endpoint.substr(0, colon_pos);
|
||||
unique_ips.emplace(ip);
|
||||
} else {
|
||||
unique_ips.emplace(extra.te_endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unique_ips.empty()) {
|
||||
LOG(WARNING) << "QueryIp: has no valid IP addresses";
|
||||
return std::vector<std::string>{};
|
||||
}
|
||||
return std::vector<std::string>(unique_ips.begin(), unique_ips.end());
|
||||
}
|
||||
|
||||
tl::expected<std::pair<size_t, size_t>, ErrorCode>
|
||||
CentralizedSegmentManager::QuerySegments(const std::string& segment) {
|
||||
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
|
||||
for (const auto& entry : mounted_segments_) {
|
||||
if (entry.second->name == segment) {
|
||||
auto mounted_seg =
|
||||
std::static_pointer_cast<MountedCentralizedSegment>(
|
||||
entry.second);
|
||||
if (mounted_seg->buf_allocator) {
|
||||
return std::make_pair(mounted_seg->buf_allocator->size(),
|
||||
mounted_seg->buf_allocator->capacity());
|
||||
} else {
|
||||
LOG(ERROR) << "unexpected null allocator";
|
||||
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG(WARNING) << "the segment doesn't exist" << ", segment=" << segment;
|
||||
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
auto CentralizedSegmentManager::OnUnmountSegment(
|
||||
const std::shared_ptr<Segment>& segment) -> tl::expected<void, ErrorCode> {
|
||||
auto mounted_seg =
|
||||
std::static_pointer_cast<MountedCentralizedSegment>(segment);
|
||||
|
||||
// Remove allocator from upper-level global_allocator_manager_ via callback
|
||||
if (mounted_seg->buf_allocator) {
|
||||
if (allocator_change_cb_) {
|
||||
allocator_change_cb_(segment->name, mounted_seg->buf_allocator,
|
||||
/*is_add=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -94,7 +94,9 @@ std::vector<Slice> split_into_slices(BufferHandle& handle) {
|
|||
|
||||
uint64_t calculate_total_size(const Replica::Descriptor& replica) {
|
||||
uint64_t total_length = 0;
|
||||
if (replica.is_memory_replica() == false) {
|
||||
if (replica.is_p2p_proxy_replica()) {
|
||||
total_length = replica.get_p2p_proxy_descriptor().object_size;
|
||||
} else if (replica.is_memory_replica() == false) {
|
||||
auto& disk_descriptor = replica.get_disk_descriptor();
|
||||
total_length = disk_descriptor.object_size;
|
||||
} else {
|
||||
|
|
@ -105,7 +107,7 @@ uint64_t calculate_total_size(const Replica::Descriptor& replica) {
|
|||
|
||||
int allocateSlices(std::vector<Slice>& slices,
|
||||
const Replica::Descriptor& replica, void* buffer_ptr) {
|
||||
if (replica.is_memory_replica() == false) {
|
||||
if (replica.is_disk_replica()) {
|
||||
// For disk-based replica, split into slices based on file size
|
||||
uint64_t offset = 0;
|
||||
uint64_t total_length = replica.get_disk_descriptor().object_size;
|
||||
|
|
@ -115,14 +117,33 @@ int allocateSlices(std::vector<Slice>& slices,
|
|||
slices.emplace_back(Slice{chunk_ptr, chunk_size});
|
||||
offset += chunk_size;
|
||||
}
|
||||
} else {
|
||||
} else if (replica.is_local_disk_replica()) {
|
||||
slices.emplace_back(
|
||||
Slice{buffer_ptr, replica.get_local_disk_descriptor().object_size});
|
||||
} else if (replica.is_memory_replica()) {
|
||||
// For memory-based replica, split into slices based on buffer
|
||||
// descriptors
|
||||
auto& handle = replica.get_memory_descriptor().buffer_descriptor;
|
||||
void* chunk_ptr = buffer_ptr;
|
||||
slices.emplace_back(Slice{chunk_ptr, handle.size_});
|
||||
} else if (replica.is_p2p_proxy_replica()) {
|
||||
slices.emplace_back(
|
||||
Slice{buffer_ptr, replica.get_p2p_proxy_descriptor().object_size});
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<Slice> BuildSlicesFromBuffers(const std::vector<void*>& buffers,
|
||||
const std::vector<size_t>& sizes,
|
||||
uint64_t total_size) {
|
||||
std::vector<Slice> slices;
|
||||
uint64_t remaining = total_size;
|
||||
for (size_t i = 0; i < buffers.size() && remaining > 0; ++i) {
|
||||
size_t chunk = std::min(static_cast<uint64_t>(sizes[i]), remaining);
|
||||
slices.emplace_back(Slice{buffers[i], chunk});
|
||||
remaining -= chunk;
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
#include "client_manager.h"
|
||||
#include "master_metric_manager.h"
|
||||
#include <glog/logging.h>
|
||||
|
||||
namespace mooncake {
|
||||
ClientManager::ClientManager(const int64_t disconnect_timeout_sec,
|
||||
const int64_t crash_timeout_sec,
|
||||
const ViewVersionId view_version)
|
||||
: view_version_(view_version) {
|
||||
ClientMeta::SetTimeouts(disconnect_timeout_sec, crash_timeout_sec);
|
||||
}
|
||||
|
||||
void ClientManager::Start() { StartClientMonitor(); }
|
||||
|
||||
void ClientManager::StartClientMonitor() {
|
||||
if (client_monitor_running_) return;
|
||||
client_monitor_running_ = true;
|
||||
client_monitor_thread_ = std::thread([this]() {
|
||||
while (client_monitor_running_) {
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(kClientMonitorSleepMs));
|
||||
if (client_monitor_running_) ClientMonitorFunc();
|
||||
}
|
||||
});
|
||||
VLOG(1) << "action=start_client_monitor_thread";
|
||||
}
|
||||
|
||||
void ClientManager::Stop() { StopClientMonitor(); }
|
||||
|
||||
void ClientManager::StopClientMonitor() {
|
||||
client_monitor_running_ = false;
|
||||
if (client_monitor_thread_.joinable()) {
|
||||
client_monitor_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
ClientManager::~ClientManager() { Stop(); }
|
||||
|
||||
auto ClientManager::GetClient(const UUID& client_id)
|
||||
-> std::shared_ptr<ClientMeta> {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
auto it = client_metas_.find(client_id);
|
||||
if (it == client_metas_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<ClientMeta>> ClientManager::GetAllClients() {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
std::vector<std::shared_ptr<ClientMeta>> clients;
|
||||
clients.reserve(client_metas_.size());
|
||||
for (const auto& [id, meta] : client_metas_) {
|
||||
clients.push_back(meta);
|
||||
}
|
||||
return clients;
|
||||
}
|
||||
|
||||
std::unique_ptr<ClientIterator> ClientManager::InnerBuildClientIterator(
|
||||
ObjectIterateStrategy strategy) {
|
||||
switch (strategy) {
|
||||
case ObjectIterateStrategy::ORDERED:
|
||||
return std::make_unique<OrderedClientIterator>(client_metas_);
|
||||
case ObjectIterateStrategy::RANDOM:
|
||||
return std::make_unique<RandomClientIterator>(client_metas_);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
auto ClientManager::ForEachClient(ObjectIterateStrategy strategy,
|
||||
const ClientVisitor& visitor)
|
||||
-> tl::expected<void, ErrorCode> {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
auto iterator = InnerBuildClientIterator(strategy);
|
||||
if (!iterator) {
|
||||
LOG(WARNING) << "fail to get client iterator"
|
||||
<< ", strategy=" << strategy;
|
||||
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
||||
}
|
||||
while (auto client = iterator->Next()) {
|
||||
auto ret = visitor(client);
|
||||
if (!ret) {
|
||||
LOG(WARNING) << "client visitor returned error"
|
||||
<< ", strategy=" << strategy
|
||||
<< ", client_id=" << client->get_client_id()
|
||||
<< ", ret=" << ret.error();
|
||||
return tl::make_unexpected(ret.error());
|
||||
}
|
||||
if (ret.value()) { // early stop
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode>
|
||||
ClientManager::GetAllSegments() {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
std::vector<std::string> all_segments;
|
||||
for (const auto& [id, meta] : client_metas_) {
|
||||
auto segments_res = meta->GetSegments();
|
||||
if (!segments_res) {
|
||||
LOG(WARNING) << "GetAllSegments: failed to get segments"
|
||||
<< ", client_id=" << id
|
||||
<< ", error=" << segments_res.error();
|
||||
continue;
|
||||
}
|
||||
for (const auto& seg : segments_res.value()) {
|
||||
all_segments.emplace_back(std::move(seg.name));
|
||||
}
|
||||
}
|
||||
return all_segments;
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode>
|
||||
ClientManager::GetClientSegments(const UUID& client_id) {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
auto it = client_metas_.find(client_id);
|
||||
if (it == client_metas_.end()) {
|
||||
LOG(WARNING) << "GetClientSegments: client not found"
|
||||
<< ", client_id=" << client_id;
|
||||
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
|
||||
}
|
||||
auto segments_res = it->second->GetSegments();
|
||||
if (!segments_res) {
|
||||
LOG(WARNING) << "GetClientSegments: failed to get segments"
|
||||
<< ", client_id=" << client_id
|
||||
<< ", error=" << segments_res.error();
|
||||
return tl::make_unexpected(segments_res.error());
|
||||
}
|
||||
std::vector<std::string> segment_names;
|
||||
segment_names.reserve(segments_res.value().size());
|
||||
for (const auto& seg : segments_res.value()) {
|
||||
segment_names.emplace_back(std::move(seg.name));
|
||||
}
|
||||
return segment_names;
|
||||
}
|
||||
|
||||
tl::expected<std::pair<size_t, size_t>, ErrorCode> ClientManager::QuerySegments(
|
||||
const std::string& segment) {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
for (const auto& [id, meta] : client_metas_) {
|
||||
auto ret = meta->QuerySegments(segment);
|
||||
if (ret.has_value()) {
|
||||
return ret;
|
||||
} else if (ret.error() != ErrorCode::SEGMENT_NOT_FOUND) {
|
||||
LOG(ERROR)
|
||||
<< "QuerySegments: failed to query segments for client_id="
|
||||
<< id << ", error=" << ret.error();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
LOG(WARNING) << "QuerySegments: segment not found"
|
||||
<< ", segment=" << segment;
|
||||
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode> ClientManager::QueryIp(
|
||||
const UUID& client_id) {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
auto it = client_metas_.find(client_id);
|
||||
if (it == client_metas_.end()) {
|
||||
LOG(WARNING) << "QueryIp: client not found"
|
||||
<< ", client_id=" << client_id;
|
||||
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
|
||||
}
|
||||
return it->second->QueryIp(client_id);
|
||||
}
|
||||
|
||||
tl::expected<std::shared_ptr<Segment>, ErrorCode> ClientManager::QuerySegment(
|
||||
const UUID& client_id, const UUID& segment_id) {
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
auto it = client_metas_.find(client_id);
|
||||
if (it == client_metas_.end()) {
|
||||
LOG(WARNING) << "QuerySegment: client not found"
|
||||
<< ", client_id=" << client_id;
|
||||
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
|
||||
}
|
||||
return it->second->QuerySegment(segment_id);
|
||||
}
|
||||
|
||||
void ClientManager::SetSegmentRemovalCallback(SegmentRemovalCallback cb) {
|
||||
segment_removal_cb_ = std::move(cb);
|
||||
}
|
||||
|
||||
auto ClientManager::RegisterClient(const RegisterClientRequest& req)
|
||||
-> tl::expected<RegisterClientResponse, ErrorCode> {
|
||||
// Architecture validation: client and master must use the same mode
|
||||
if (req.deployment_mode != GetDeploymentMode()) {
|
||||
LOG(ERROR) << "RegisterClient: architecture mismatch"
|
||||
<< ", client_mode=" << static_cast<int>(req.deployment_mode)
|
||||
<< ", master_mode=" << static_cast<int>(GetDeploymentMode())
|
||||
<< ", client_id=" << req.client_id;
|
||||
return tl::make_unexpected(ErrorCode::ILLEGAL_CLIENT);
|
||||
}
|
||||
|
||||
const auto& client_id = req.client_id;
|
||||
auto it = client_metas_.find(client_id);
|
||||
if (it != client_metas_.end()) {
|
||||
LOG(WARNING) << "RegisterClient: client already exists"
|
||||
<< ", client_id=" << client_id;
|
||||
return tl::make_unexpected(ErrorCode::CLIENT_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
auto meta = CreateClientMeta(req);
|
||||
if (segment_removal_cb_) {
|
||||
meta->SetSegmentRemovalCallback(segment_removal_cb_);
|
||||
}
|
||||
for (const auto& segment : req.segments) {
|
||||
auto result = meta->MountSegment(segment);
|
||||
if (!result) {
|
||||
LOG(ERROR) << "RegisterClient: failed to mount segment"
|
||||
<< ", segment_name=" << segment.name
|
||||
<< ", client_id=" << client_id
|
||||
<< ", error=" << result.error();
|
||||
return tl::make_unexpected(result.error());
|
||||
}
|
||||
}
|
||||
|
||||
OnClientRegistered(meta);
|
||||
|
||||
SharedMutexLocker lock(&clients_mutex_);
|
||||
// Write to client_metas_ (overwrites if re-registering after crash)
|
||||
client_metas_[client_id] = std::move(meta);
|
||||
|
||||
MasterMetricManager::instance().inc_active_clients();
|
||||
|
||||
RegisterClientResponse response;
|
||||
response.view_version = view_version_;
|
||||
|
||||
LOG(INFO) << "RegisterClient: client_id=" << client_id
|
||||
<< ", segments=" << req.segments.size()
|
||||
<< ", view_version=" << response.view_version;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
auto ClientManager::Heartbeat(const HeartbeatRequest& req)
|
||||
-> tl::expected<HeartbeatResponse, ErrorCode> {
|
||||
const auto& client_id = req.client_id;
|
||||
HeartbeatResponse response;
|
||||
response.view_version = view_version_;
|
||||
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
auto it = client_metas_.find(client_id);
|
||||
if (it == client_metas_.end()) {
|
||||
// Client not in client_metas_: master restarted or client heartbeat
|
||||
// timed out and the meta of client was cleaned up. Return UNDEFINED +
|
||||
// view_version to inform client to re-register.
|
||||
response.status = ClientStatus::UNDEFINED;
|
||||
return response;
|
||||
}
|
||||
|
||||
auto& meta = it->second;
|
||||
|
||||
// Update Heartbeat
|
||||
auto [old_status, new_status] = meta->Heartbeat();
|
||||
response.status = new_status;
|
||||
if (new_status == ClientStatus::HEALTH) {
|
||||
if (old_status != new_status) {
|
||||
LOG(INFO) << "client recovered"
|
||||
<< ", client_id=" << client_id;
|
||||
meta->OnRecovered();
|
||||
}
|
||||
for (const auto& task : req.tasks) {
|
||||
response.task_results.push_back(ProcessTask(client_id, task));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
auto ClientManager::QueryClientStatus(const QueryClientStatusRequest& req)
|
||||
-> tl::expected<QueryClientStatusResponse, ErrorCode> {
|
||||
const auto& client_id = req.client_id;
|
||||
QueryClientStatusResponse response;
|
||||
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
auto it = client_metas_.find(client_id);
|
||||
if (it == client_metas_.end()) {
|
||||
response.status = ClientStatus::UNDEFINED;
|
||||
} else {
|
||||
response.status = it->second->get_health_state().status;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// 1. Phase 1 (Shared Lock): Check health status
|
||||
// 2. Phase 2 (No Lock): Execute crashed client hooks
|
||||
// 3. Phase 3 (Write Lock): Clean up crashed clients
|
||||
void ClientManager::ClientMonitorFunc() {
|
||||
// Attention:
|
||||
// 1. DISCONNECTED is not finnal status. The clients in
|
||||
// newly_disconnected might change its status.
|
||||
// 2. CRASHED is finnal status. The clients in newly_crashed will always
|
||||
// be crashed.
|
||||
std::vector<std::shared_ptr<ClientMeta>> newly_disconnected;
|
||||
std::vector<std::shared_ptr<ClientMeta>> newly_crashed;
|
||||
|
||||
// Phase 1: Check health status
|
||||
{
|
||||
SharedMutexLocker lock(&clients_mutex_, shared_lock);
|
||||
for (auto& [client_id, meta] : client_metas_) {
|
||||
auto [old_status, new_status] = meta->CheckHealth();
|
||||
if (old_status != new_status) {
|
||||
if (new_status == ClientStatus::DISCONNECTION) {
|
||||
newly_disconnected.push_back(meta);
|
||||
} else if (new_status == ClientStatus::CRASHED) {
|
||||
newly_crashed.push_back(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Execute hooks (No client_mutex lock)
|
||||
// We can safely execute hooks because we hold shared_ptrs to
|
||||
// ClientMeta, so they won't be destroyed.
|
||||
// And hooks don't need client_mutex because they are protected by
|
||||
// client_meta itself
|
||||
for (const auto& client : newly_disconnected) {
|
||||
// The client might change to Healthy by concurrent heartbeat.
|
||||
// So OnDisconnected() need to check the status again.
|
||||
client->OnDisconnected();
|
||||
}
|
||||
|
||||
for (const auto& client : newly_crashed) {
|
||||
client->OnCrashed();
|
||||
}
|
||||
|
||||
// Phase 3: Clean up crashed clients (Write Lock)
|
||||
if (!newly_crashed.empty()) {
|
||||
SharedMutexLocker lock(&clients_mutex_);
|
||||
for (const auto& client : newly_crashed) {
|
||||
client_metas_.erase(client->get_client_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
#include "client_meta.h"
|
||||
#include <glog/logging.h>
|
||||
#include "master_metric_manager.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
// Define static timeout members
|
||||
int64_t ClientMeta::disconnect_timeout_sec_ = 0;
|
||||
int64_t ClientMeta::crash_timeout_sec_ = 0;
|
||||
|
||||
ClientMeta::ClientMeta(const UUID& client_id) : client_id_(client_id) {
|
||||
health_state_.status = ClientStatus::HEALTH;
|
||||
health_state_.last_heartbeat = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> ClientMeta::MountSegment(const Segment& segment) {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return check_ret;
|
||||
}
|
||||
|
||||
auto ret = GetSegmentManager()->MountSegment(segment);
|
||||
if (!ret.has_value()) {
|
||||
if (ret.error() == ErrorCode::SEGMENT_ALREADY_EXISTS) {
|
||||
LOG(WARNING) << "attempt to mount segment but it already exists"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", segment_id =" << segment.id
|
||||
<< ", segment_name=" << segment.name
|
||||
<< ", ret=" << ret.error();
|
||||
return {}; // ignore the errcode
|
||||
} else {
|
||||
LOG(ERROR) << "fail to mount segment"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", segment_id =" << segment.id
|
||||
<< ", segment_name=" << segment.name
|
||||
<< ", ret=" << ret.error();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
LOG(INFO) << "Mount segment success"
|
||||
<< ", client_id=" << client_id_ << ", segment_id =" << segment.id
|
||||
<< ", segment_name=" << segment.name;
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> ClientMeta::UnmountSegment(
|
||||
const UUID& segment_id) {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return check_ret;
|
||||
}
|
||||
auto ret = GetSegmentManager()->UnmountSegment(segment_id);
|
||||
if (!ret.has_value()) {
|
||||
if (ret.error() == ErrorCode::SEGMENT_NOT_FOUND) {
|
||||
LOG(WARNING) << "attempt to unmount segment but it does not exist"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", segment_id=" << segment_id
|
||||
<< ", ret=" << ret.error();
|
||||
return {}; // ignore the errcode
|
||||
} else {
|
||||
LOG(ERROR) << "fail to unmount segment"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", segment_id=" << segment_id
|
||||
<< ", ret=" << ret.error();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
LOG(INFO) << "Unmount segment success"
|
||||
<< ", client_id=" << client_id_ << ", segment_id =" << segment_id;
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<std::vector<Segment>, ErrorCode> ClientMeta::GetSegments() {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return tl::make_unexpected(check_ret.error());
|
||||
}
|
||||
return GetSegmentManager()->GetSegments();
|
||||
}
|
||||
|
||||
tl::expected<std::pair<size_t, size_t>, ErrorCode> ClientMeta::QuerySegments(
|
||||
const std::string& segment_name) {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return tl::make_unexpected(check_ret.error());
|
||||
}
|
||||
return GetSegmentManager()->QuerySegments(segment_name);
|
||||
}
|
||||
|
||||
tl::expected<std::shared_ptr<Segment>, ErrorCode> ClientMeta::QuerySegment(
|
||||
const UUID& segment_id) {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return tl::make_unexpected(check_ret.error());
|
||||
}
|
||||
return GetSegmentManager()->QuerySegment(segment_id);
|
||||
}
|
||||
|
||||
void ClientMeta::SetSegmentRemovalCallback(SegmentRemovalCallback cb) {
|
||||
GetSegmentManager()->SetSegmentRemovalCallback(std::move(cb));
|
||||
}
|
||||
|
||||
void ClientMeta::SetTimeouts(int64_t disconnect_sec, int64_t crash_sec) {
|
||||
disconnect_timeout_sec_ = disconnect_sec;
|
||||
crash_timeout_sec_ = crash_sec;
|
||||
}
|
||||
|
||||
ClientHealthState ClientMeta::get_health_state() const {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
return health_state_;
|
||||
}
|
||||
|
||||
bool ClientMeta::is_health() const {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
return health_state_.status == ClientStatus::HEALTH;
|
||||
}
|
||||
|
||||
std::pair<ClientStatus, ClientStatus> ClientMeta::Heartbeat() {
|
||||
SharedMutexLocker lock(&client_mutex_);
|
||||
InnerUpdateHeartbeat();
|
||||
return InnerUpdateHealthStatus();
|
||||
}
|
||||
|
||||
std::pair<ClientStatus, ClientStatus> ClientMeta::CheckHealth() {
|
||||
SharedMutexLocker lock(&client_mutex_);
|
||||
return InnerUpdateHealthStatus();
|
||||
}
|
||||
|
||||
void ClientMeta::InnerUpdateHeartbeat() {
|
||||
if (health_state_.status == ClientStatus::CRASHED) {
|
||||
LOG(WARNING) << "heartbeat received while in CRASHED state, "
|
||||
"timestamp will not update"
|
||||
<< ", client_id=" << client_id_;
|
||||
return;
|
||||
} else if (health_state_.status == ClientStatus::DISCONNECTION) {
|
||||
LOG(WARNING) << "heartbeat received while in DISCONNECTION state, "
|
||||
"the state might change to HEALTH as soon as possible"
|
||||
<< ", client_id=" << client_id_;
|
||||
}
|
||||
health_state_.last_heartbeat = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
std::pair<ClientStatus, ClientStatus> ClientMeta::InnerUpdateHealthStatus() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
ClientStatus old_status = health_state_.status;
|
||||
|
||||
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - health_state_.last_heartbeat)
|
||||
.count();
|
||||
|
||||
int64_t disconnect_timeout_ms = disconnect_timeout_sec_ * 1000;
|
||||
int64_t crash_timeout_ms = crash_timeout_sec_ * 1000;
|
||||
|
||||
switch (health_state_.status) {
|
||||
case ClientStatus::HEALTH: {
|
||||
if (elapsed_ms >= disconnect_timeout_ms) {
|
||||
if (elapsed_ms >= crash_timeout_ms) {
|
||||
health_state_.status = ClientStatus::CRASHED;
|
||||
} else {
|
||||
health_state_.status = ClientStatus::DISCONNECTION;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClientStatus::DISCONNECTION: {
|
||||
if (elapsed_ms < disconnect_timeout_ms) {
|
||||
health_state_.status = ClientStatus::HEALTH;
|
||||
} else if (elapsed_ms >= crash_timeout_ms) {
|
||||
health_state_.status = ClientStatus::CRASHED;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClientStatus::CRASHED:
|
||||
case ClientStatus::UNDEFINED:
|
||||
// final states, do nothing
|
||||
break;
|
||||
}
|
||||
if (health_state_.status != old_status) {
|
||||
// client status changed
|
||||
LOG(INFO) << "Client status changed"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", old_status=" << HealthToString(old_status)
|
||||
<< ", new_status=" << HealthToString(health_state_.status);
|
||||
}
|
||||
return {old_status, health_state_.status};
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> ClientMeta::InnerStatusCheck() const {
|
||||
if (health_state_.status != ClientStatus::HEALTH) {
|
||||
LOG(WARNING) << "Client is not HEALTH"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", status=" << HealthToString(health_state_.status);
|
||||
return tl::make_unexpected(ErrorCode::CLIENT_UNHEALTHY);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void ClientMeta::OnDisconnected() {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
if (health_state_.status == ClientStatus::HEALTH) {
|
||||
// concurrent heartbeat might have recovered the client, skip
|
||||
return;
|
||||
} else if (health_state_.status != ClientStatus::DISCONNECTION) {
|
||||
LOG(ERROR) << "unexpected hook calling" << ", client_id=" << client_id_
|
||||
<< ", current status="
|
||||
<< HealthToString(health_state_.status)
|
||||
<< ", expected status="
|
||||
<< HealthToString(ClientStatus::DISCONNECTION);
|
||||
return;
|
||||
}
|
||||
LOG(INFO) << "the client is disconnected" << ", client_id=" << client_id_;
|
||||
DoOnDisconnected();
|
||||
MasterMetricManager::instance().dec_active_clients();
|
||||
}
|
||||
|
||||
void ClientMeta::OnRecovered() {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
if (health_state_.status != ClientStatus::HEALTH) {
|
||||
LOG(ERROR) << "unexpected hook calling" << ", client_id=" << client_id_
|
||||
<< ", current status="
|
||||
<< HealthToString(health_state_.status)
|
||||
<< ", expected status="
|
||||
<< HealthToString(ClientStatus::HEALTH);
|
||||
return;
|
||||
}
|
||||
LOG(INFO) << "the client is recovered" << ", client_id=" << client_id_;
|
||||
DoOnRecovered();
|
||||
MasterMetricManager::instance().inc_active_clients();
|
||||
}
|
||||
|
||||
void ClientMeta::OnCrashed() {
|
||||
LOG(INFO) << "the client is crashed, start to recycle meta"
|
||||
<< ", client_id=" << client_id_;
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto segments_res = GetSegmentManager()->GetSegments();
|
||||
if (segments_res) {
|
||||
for (const auto& seg : *segments_res) {
|
||||
auto ret = GetSegmentManager()->UnmountSegment(seg.id);
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "Failed to unmount segment"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", segment_id=" << seg.id
|
||||
<< " error=" << ret.error();
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG(INFO) << "the client meta is recycled over"
|
||||
<< ", client_id=" << client_id_;
|
||||
}
|
||||
|
||||
std::string ClientMeta::HealthToString(ClientStatus status) const {
|
||||
switch (status) {
|
||||
case ClientStatus::HEALTH:
|
||||
return "HEALTH";
|
||||
case ClientStatus::DISCONNECTION:
|
||||
return "DISCONNECTION";
|
||||
case ClientStatus::CRASHED:
|
||||
return "CRASHED";
|
||||
case ClientStatus::UNDEFINED:
|
||||
return "UNDEFINED";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
#include "client_rpc_service.h"
|
||||
#include <glog/logging.h>
|
||||
#include <ylt/coro_rpc/coro_rpc_server.hpp>
|
||||
#include "utils/scoped_vlog_timer.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
ClientRpcService::ClientRpcService(DataManager& data_manager)
|
||||
: data_manager_(data_manager) {}
|
||||
|
||||
tl::expected<void, ErrorCode> ClientRpcService::ReadRemoteData(
|
||||
const RemoteReadRequest& request) {
|
||||
ScopedVLogTimer timer(1, "ClientRpcService::ReadRemoteData");
|
||||
timer.LogRequest("key=", request.key,
|
||||
"buffer_count=", request.dest_buffers.size());
|
||||
|
||||
if (request.key.empty()) {
|
||||
LOG(ERROR) << "ReadRemoteData: empty key";
|
||||
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
|
||||
if (request.dest_buffers.empty()) {
|
||||
LOG(ERROR) << "ReadRemoteData: empty destination buffers";
|
||||
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
|
||||
// Validate buffers (segment name validation is done in DataManager)
|
||||
for (const auto& buffer_desc : request.dest_buffers) {
|
||||
if (buffer_desc.size == 0 || buffer_desc.addr == 0) {
|
||||
LOG(ERROR)
|
||||
<< "ReadRemoteData: invalid buffer (zero size or null address)";
|
||||
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate to DataManager
|
||||
auto result =
|
||||
data_manager_.ReadRemoteData(request.key, request.dest_buffers);
|
||||
|
||||
if (!result.has_value()) {
|
||||
LOG(ERROR) << "ReadRemoteData failed for key: " << request.key
|
||||
<< ", error: " << toString(result.error());
|
||||
timer.LogResponse("error_code=", result.error());
|
||||
|
||||
// Rectify stale route when key not found
|
||||
if (result.error() == ErrorCode::OBJECT_NOT_FOUND) {
|
||||
data_manager_.RectifyReadRoute(request.key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
timer.LogResponse("error_code=", ErrorCode::OK);
|
||||
return {};
|
||||
}
|
||||
|
||||
tl::expected<UUID, ErrorCode> ClientRpcService::WriteRemoteData(
|
||||
const RemoteWriteRequest& request) {
|
||||
ScopedVLogTimer timer(1, "ClientRpcService::WriteRemoteData");
|
||||
timer.LogRequest("key=", request.key,
|
||||
"buffer_count=", request.src_buffers.size());
|
||||
|
||||
if (request.key.empty()) {
|
||||
LOG(ERROR) << "WriteRemoteData: empty key";
|
||||
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
|
||||
if (request.src_buffers.empty()) {
|
||||
LOG(ERROR) << "WriteRemoteData: empty source buffers";
|
||||
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
|
||||
// Validate buffers (segment name validation is done in DataManager)
|
||||
for (const auto& buffer_desc : request.src_buffers) {
|
||||
if (buffer_desc.size == 0 || buffer_desc.addr == 0) {
|
||||
LOG(ERROR) << "WriteRemoteData: invalid buffer (zero size or null "
|
||||
"address)";
|
||||
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
|
||||
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate to DataManager
|
||||
auto result = data_manager_.WriteRemoteData(
|
||||
request.key, request.src_buffers, request.target_tier_id);
|
||||
|
||||
if (!result.has_value()) {
|
||||
LOG(ERROR) << "WriteRemoteData failed for key: " << request.key
|
||||
<< ", error: " << toString(result.error());
|
||||
timer.LogResponse("error_code=", result.error());
|
||||
return result;
|
||||
}
|
||||
|
||||
timer.LogResponse("error_code=", ErrorCode::OK);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RegisterClientRpcService(coro_rpc::coro_rpc_server& server,
|
||||
ClientRpcService& service) {
|
||||
server.register_handler<&ClientRpcService::ReadRemoteData>(&service);
|
||||
server.register_handler<&ClientRpcService::WriteRemoteData>(&service);
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -262,11 +262,13 @@ ErrorCode DummyClient::connect(const std::string& server_address) {
|
|||
auto pool = client_accessor_.GetClientPool();
|
||||
// The client pool does not have native connection check method, so we need
|
||||
// to use custom ServiceReady API.
|
||||
auto result = invoke_rpc<&RealClient::service_ready_internal, void>();
|
||||
auto result =
|
||||
invoke_rpc<&RealClient::service_ready_internal, DeploymentMode>();
|
||||
if (!result.has_value()) {
|
||||
timer.LogResponse("error_code=", result.error());
|
||||
return result.error();
|
||||
}
|
||||
deployment_mode_ = result.value();
|
||||
timer.LogResponse("error_code=", ErrorCode::OK);
|
||||
connected_ = true;
|
||||
return ErrorCode::OK;
|
||||
|
|
@ -341,11 +343,9 @@ int DummyClient::register_shm_via_ipc(const ShmHelper::ShmSegment* shm,
|
|||
return 0;
|
||||
}
|
||||
|
||||
int DummyClient::setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
|
||||
const std::string& server_address,
|
||||
const std::string& ipc_socket_path) {
|
||||
int DummyClient::setup(DummyClientConfig& config) {
|
||||
void* base_addr = nullptr;
|
||||
ErrorCode err = connect(server_address);
|
||||
ErrorCode err = connect(config.real_client_addr);
|
||||
if (err != ErrorCode::OK) {
|
||||
LOG(ERROR) << "Failed to connect to real client";
|
||||
return -1;
|
||||
|
|
@ -353,13 +353,13 @@ int DummyClient::setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
|
|||
|
||||
shm_helper_ = ShmHelper::getInstance();
|
||||
try {
|
||||
base_addr = shm_helper_->allocate(local_buffer_size);
|
||||
base_addr = shm_helper_->allocate(config.local_buffer_size);
|
||||
} catch (const std::exception& e) {
|
||||
LOG(ERROR) << "Failed to allocate shared memory: " << e.what();
|
||||
return -1;
|
||||
}
|
||||
|
||||
ipc_socket_path_ = ipc_socket_path;
|
||||
ipc_socket_path_ = config.ipc_socket_path;
|
||||
|
||||
// Attempt registration for the primary segment
|
||||
auto local_buffer_shm = shm_helper_->get_shm(base_addr);
|
||||
|
|
@ -475,21 +475,21 @@ uint64_t DummyClient::alloc_from_mem_pool(size_t size) {
|
|||
}
|
||||
|
||||
int DummyClient::put(const std::string& key, std::span<const char> value,
|
||||
const ReplicateConfig& config) {
|
||||
const WriteConfig& config) {
|
||||
return to_py_ret(invoke_rpc<&RealClient::put_dummy_helper, void>(
|
||||
key, value, config, client_id_));
|
||||
}
|
||||
|
||||
int DummyClient::put_batch(const std::vector<std::string>& keys,
|
||||
const std::vector<std::span<const char>>& values,
|
||||
const ReplicateConfig& config) {
|
||||
const WriteConfig& config) {
|
||||
return to_py_ret(invoke_rpc<&RealClient::put_batch_dummy_helper, void>(
|
||||
keys, values, config, client_id_));
|
||||
}
|
||||
|
||||
int DummyClient::put_parts(const std::string& key,
|
||||
std::vector<std::span<const char>> values,
|
||||
const ReplicateConfig& config) {
|
||||
const WriteConfig& config) {
|
||||
return to_py_ret(invoke_rpc<&RealClient::put_parts_dummy_helper, void>(
|
||||
key, values, config, client_id_));
|
||||
}
|
||||
|
|
@ -541,15 +541,17 @@ int64_t DummyClient::getSize(const std::string& key) {
|
|||
return to_py_ret(invoke_rpc<&RealClient::getSize_internal, int64_t>(key));
|
||||
}
|
||||
|
||||
std::shared_ptr<BufferHandle> DummyClient::get_buffer(const std::string& key) {
|
||||
std::shared_ptr<BufferHandle> DummyClient::get_buffer(
|
||||
const std::string& key, const ReadRouteConfig& config) {
|
||||
// Dummy client does not use BufferHandle, so we return nullptr
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::tuple<uint64_t, size_t> DummyClient::get_buffer_info(
|
||||
const std::string& key) {
|
||||
auto result = invoke_rpc<&RealClient::get_buffer_info_dummy_helper,
|
||||
std::tuple<uint64_t, size_t>>(key, client_id_);
|
||||
const std::string& key, const ReadRouteConfig& config) {
|
||||
auto result =
|
||||
invoke_rpc<&RealClient::get_buffer_info_dummy_helper,
|
||||
std::tuple<uint64_t, size_t>>(key, config, client_id_);
|
||||
if (!result.has_value()) {
|
||||
LOG(ERROR) << "Get buffer failed: " << toString(result.error());
|
||||
return std::make_tuple(0, 0);
|
||||
|
|
@ -558,13 +560,13 @@ std::tuple<uint64_t, size_t> DummyClient::get_buffer_info(
|
|||
}
|
||||
|
||||
std::vector<std::shared_ptr<BufferHandle>> DummyClient::batch_get_buffer(
|
||||
const std::vector<std::string>& keys) {
|
||||
const std::vector<std::string>& keys, const ReadRouteConfig& config) {
|
||||
// TODO: implement this function
|
||||
return std::vector<std::shared_ptr<BufferHandle>>();
|
||||
}
|
||||
|
||||
int64_t DummyClient::get_into(const std::string& key, void* buffer,
|
||||
size_t size) {
|
||||
int64_t DummyClient::get_into(const std::string& key, void* buffer, size_t size,
|
||||
const ReadRouteConfig& config) {
|
||||
// TODO: implement this function
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -576,7 +578,7 @@ std::string DummyClient::get_hostname() const {
|
|||
|
||||
std::vector<int> DummyClient::batch_put_from(
|
||||
const std::vector<std::string>& keys, const std::vector<void*>& buffer_ptrs,
|
||||
const std::vector<size_t>& sizes, const ReplicateConfig& config) {
|
||||
const std::vector<size_t>& sizes, const WriteConfig& config) {
|
||||
std::vector<uint64_t> buffers;
|
||||
for (auto ptr : buffer_ptrs) {
|
||||
buffers.push_back(reinterpret_cast<uint64_t>(ptr));
|
||||
|
|
@ -595,21 +597,21 @@ std::vector<int> DummyClient::batch_put_from(
|
|||
}
|
||||
|
||||
int DummyClient::put_from(const std::string& key, void* buffer, size_t size,
|
||||
const ReplicateConfig& config) {
|
||||
const WriteConfig& config) {
|
||||
// TODO: implement this function
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<int64_t> DummyClient::batch_get_into(
|
||||
const std::vector<std::string>& keys, const std::vector<void*>& buffer_ptrs,
|
||||
const std::vector<size_t>& sizes) {
|
||||
const std::vector<size_t>& sizes, const ReadRouteConfig& config) {
|
||||
std::vector<uint64_t> buffers;
|
||||
for (auto ptr : buffer_ptrs) {
|
||||
buffers.push_back(reinterpret_cast<uint64_t>(ptr));
|
||||
}
|
||||
auto internal_results =
|
||||
invoke_batch_rpc<&RealClient::batch_get_into_dummy_helper, int64_t>(
|
||||
keys.size(), keys, buffers, sizes, client_id_);
|
||||
keys.size(), keys, buffers, sizes, config, client_id_);
|
||||
std::vector<int64_t> results;
|
||||
results.reserve(internal_results.size());
|
||||
|
||||
|
|
@ -623,7 +625,7 @@ std::vector<int64_t> DummyClient::batch_get_into(
|
|||
int DummyClient::put_from_with_metadata(const std::string& key, void* buffer,
|
||||
void* metadata_buffer, size_t size,
|
||||
size_t metadata_size,
|
||||
const ReplicateConfig& config) {
|
||||
const WriteConfig& config) {
|
||||
// TODO: implement this function
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -632,7 +634,7 @@ std::vector<int> DummyClient::batch_put_from_multi_buffers(
|
|||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffer_ptrs,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
const ReplicateConfig& config) {
|
||||
const WriteConfig& config) {
|
||||
// TODO: implement this function
|
||||
std::vector<int> vec(keys.size(), -1);
|
||||
return vec;
|
||||
|
|
@ -642,7 +644,7 @@ std::vector<int> DummyClient::batch_get_into_multi_buffers(
|
|||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<void*>>& all_buffer_ptrs,
|
||||
const std::vector<std::vector<size_t>>& all_sizes,
|
||||
bool prefer_alloc_in_same_node) {
|
||||
bool aggregate_same_segment_task, const ReadRouteConfig& config) {
|
||||
// TODO: implement this function
|
||||
std::vector<int> vec(keys.size(), -1);
|
||||
return vec;
|
||||
|
|
@ -689,10 +691,10 @@ void DummyClient::ping_thread_main() {
|
|||
|
||||
while (ping_running_) {
|
||||
auto ping_result =
|
||||
invoke_rpc<&RealClient::ping, PingResponse>(client_id_);
|
||||
invoke_rpc<&RealClient::ping, HeartbeatResponse>(client_id_);
|
||||
|
||||
if (ping_result.has_value() &&
|
||||
ping_result.value().client_status == ClientStatus::OK) {
|
||||
ping_result.value().status == ClientStatus::HEALTH) {
|
||||
ping_fail_count = 0;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(success_ping_interval_ms));
|
||||
|
|
@ -734,7 +736,8 @@ void DummyClient::ping_thread_main() {
|
|||
// Even if register_shm_via_ipc succeeded, we should check
|
||||
// if RPC is responsive
|
||||
auto check_rpc =
|
||||
invoke_rpc<&RealClient::ping, PingResponse>(client_id_);
|
||||
invoke_rpc<&RealClient::ping, HeartbeatResponse>(
|
||||
client_id_);
|
||||
if (check_rpc.has_value()) {
|
||||
LOG(INFO) << "RPC connection restored";
|
||||
ping_fail_count = 0;
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ bool FileStorageConfig::Validate() const {
|
|||
return true;
|
||||
}
|
||||
|
||||
FileStorage::FileStorage(std::shared_ptr<Client> client,
|
||||
FileStorage::FileStorage(std::shared_ptr<CentralizedClientService> client,
|
||||
const std::string& local_rpc_addr,
|
||||
const FileStorageConfig& config)
|
||||
: client_(client),
|
||||
|
|
@ -390,7 +390,7 @@ tl::expected<void, ErrorCode> FileStorage::BatchQuerySegmentSlices(
|
|||
for (size_t i = 0; i < batched_query_results.size(); ++i) {
|
||||
if (batched_query_results[i]) {
|
||||
for (const auto& descriptor :
|
||||
batched_query_results[i].value().replicas) {
|
||||
batched_query_results[i].value()->replicas) {
|
||||
if (descriptor.is_memory_replica()) {
|
||||
const auto& memory_descriptor =
|
||||
descriptor.get_memory_descriptor();
|
||||
|
|
@ -436,8 +436,10 @@ tl::expected<FileStorage::AllocatedBatch, ErrorCode> FileStorage::AllocateBatch(
|
|||
const std::vector<std::string>& keys, const std::vector<int64_t>& sizes) {
|
||||
AllocatedBatch result;
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
assert(sizes[i] <= kMaxSliceSize);
|
||||
auto alloc_result = client_buffer_allocator_->allocate(sizes[i]);
|
||||
assert(sizes[i] >= 0);
|
||||
assert(static_cast<uint64_t>(sizes[i]) <= kMaxSliceSize);
|
||||
auto alloc_result =
|
||||
client_buffer_allocator_->allocate(static_cast<size_t>(sizes[i]));
|
||||
if (!alloc_result) {
|
||||
LOG(ERROR) << "Failed to allocate slice buffer, size = " << sizes[i]
|
||||
<< ", key = " << keys[i];
|
||||
|
|
@ -450,4 +452,4 @@ tl::expected<FileStorage::AllocatedBatch, ErrorCode> FileStorage::AllocateBatch(
|
|||
return result;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
} // namespace mooncake
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "ha_helper.h"
|
||||
#include "etcd_helper.h"
|
||||
#include "rpc_service.h"
|
||||
#include "centralized_rpc_service.h"
|
||||
#include "p2p_rpc_service.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
|
|
@ -152,9 +153,16 @@ int MasterServiceSupervisor::Start() {
|
|||
std::this_thread::sleep_for(std::chrono::seconds(waiting_time));
|
||||
|
||||
LOG(INFO) << "Starting master service...";
|
||||
mooncake::WrappedMasterService wrapped_master_service(
|
||||
mooncake::WrappedMasterServiceConfig(config_, view_version));
|
||||
mooncake::RegisterRpcService(server, wrapped_master_service);
|
||||
if (config_.deployment_mode == DeploymentMode::CENTRALIZATION) {
|
||||
WrappedCentralizedMasterService wrapped_master_service(
|
||||
WrappedMasterServiceConfig(config_, view_version));
|
||||
RegisterCentralizedRpcService(server, wrapped_master_service);
|
||||
} else {
|
||||
WrappedP2PMasterService wrapped_master_service(
|
||||
WrappedMasterServiceConfig(config_, view_version));
|
||||
|
||||
RegisterP2PRpcService(server, wrapped_master_service);
|
||||
}
|
||||
// Metric reporting is now handled by WrappedMasterService.
|
||||
|
||||
async_simple::Future<coro_rpc::err_code> ec =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,301 @@
|
|||
#include "ha_recovery_manager.h"
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
HARecoveryManager::HARecoveryManager(
|
||||
const UUID& client_id, P2PMasterClient& master_client,
|
||||
std::optional<DataManager>& data_manager,
|
||||
std::unique_ptr<AsyncMetadataNotifier>& notifier,
|
||||
std::atomic<ViewVersionId>& view_version, HAClientState initial_state)
|
||||
: client_id_(client_id),
|
||||
master_client_(master_client),
|
||||
data_manager_(data_manager),
|
||||
notifier_(notifier),
|
||||
view_version_(view_version),
|
||||
state_(initial_state) {
|
||||
LOG(INFO) << "HA recovery manager initialized with state: "
|
||||
<< initial_state;
|
||||
}
|
||||
|
||||
HARecoveryManager::~HARecoveryManager() { Stop(); }
|
||||
|
||||
void HARecoveryManager::Stop() {
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
if (need_abort_) {
|
||||
need_abort_->store(true, std::memory_order_release);
|
||||
abort_cv_.notify_all();
|
||||
}
|
||||
if (recovery_thread_.joinable()) {
|
||||
recovery_thread_.join();
|
||||
}
|
||||
if (state_.load(std::memory_order_relaxed) == HAClientState::SYNCING) {
|
||||
TransitionState(HAClientState::DEGRADED, "shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> HARecoveryManager::SetSyncCompleted() {
|
||||
auto result = master_client_.SetSyncCompleted(client_id_);
|
||||
if (!result) {
|
||||
LOG(ERROR) << "SetSyncCompleted RPC failed: " << result.error();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State Machine
|
||||
// ============================================================================
|
||||
|
||||
void HARecoveryManager::TransitionState(HAClientState to,
|
||||
const std::string& reason) {
|
||||
auto from = state_.load(std::memory_order_relaxed);
|
||||
LOG(WARNING) << "HA state: " << from << " -> " << to
|
||||
<< ", reason=" << reason
|
||||
<< ", view_version=" << view_version_.load();
|
||||
state_.store(to, std::memory_order_release);
|
||||
}
|
||||
|
||||
void HARecoveryManager::HandleEvent(HAEvent event) {
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
if (event == HAEvent::MASTER_UNREACHABLE &&
|
||||
state_.load(std::memory_order_acquire) == HAClientState::DEGRADED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort + join under mutex_. Safe because recovery thread's exit path
|
||||
// uses lock-free CAS (no mutex_ needed), so no deadlock.
|
||||
if (need_abort_) {
|
||||
need_abort_->store(true, std::memory_order_release);
|
||||
abort_cv_.notify_all();
|
||||
}
|
||||
if (recovery_thread_.joinable()) {
|
||||
recovery_thread_.join();
|
||||
}
|
||||
|
||||
auto current = state_.load(std::memory_order_relaxed);
|
||||
|
||||
switch (event) {
|
||||
case HAEvent::MASTER_UNREACHABLE:
|
||||
if (current == HAClientState::FULL ||
|
||||
current == HAClientState::SYNCING) {
|
||||
TransitionState(HAClientState::DEGRADED,
|
||||
"heartbeat failure threshold exceeded");
|
||||
if (notifier_) notifier_->Stop(/*drop_pending=*/true);
|
||||
}
|
||||
break;
|
||||
|
||||
case HAEvent::MASTER_RECONNECTED:
|
||||
if (current == HAClientState::DEGRADED) {
|
||||
if (notifier_) notifier_->Start();
|
||||
}
|
||||
if (current != HAClientState::SYNCING) {
|
||||
TransitionState(HAClientState::SYNCING,
|
||||
"master connection ready, starting recovery");
|
||||
}
|
||||
StartRecoveryThread();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Recovery Thread Management
|
||||
// ============================================================================
|
||||
|
||||
void HARecoveryManager::StartRecoveryThread() {
|
||||
// Old thread already joined by HandleEvent before acquiring mutex_.
|
||||
// Create fresh abort token and start new thread.
|
||||
need_abort_ = std::make_shared<std::atomic<bool>>(false);
|
||||
auto need_abort = need_abort_;
|
||||
recovery_thread_ =
|
||||
std::thread([this, need_abort]() { RecoveryPipelineMain(need_abort); });
|
||||
}
|
||||
|
||||
bool HARecoveryManager::WaitForReady(const AbortToken& need_abort) {
|
||||
// Wait for P2PClientService::Init to complete (InitStorage initializes
|
||||
// data_manager_, and Init calls SetReadyForRecovery() at the end).
|
||||
// This avoids data race on data_manager_ which is an std::optional.
|
||||
while (!ready_for_recovery_.load(std::memory_order_acquire) &&
|
||||
!need_abort->load(std::memory_order_acquire)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
if (need_abort->load(std::memory_order_acquire)) {
|
||||
LOG(INFO) << "Recovery pipeline aborted during wait for ready signal";
|
||||
return false;
|
||||
}
|
||||
|
||||
// After ready_for_recovery_ is set, data_manager_ should be initialized.
|
||||
if (!data_manager_.has_value()) {
|
||||
LOG(ERROR) << "DataManager not initialized, cannot run recovery";
|
||||
TransitionState(HAClientState::DEGRADED,
|
||||
"data_manager not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG(INFO) << "Recovery pipeline proceeding, service ready";
|
||||
return true;
|
||||
}
|
||||
|
||||
void HARecoveryManager::RecoveryPipelineMain(AbortToken need_abort) {
|
||||
LOG(INFO) << "Recovery pipeline started";
|
||||
|
||||
if (!WaitForReady(need_abort)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto aborted = [&]() {
|
||||
return need_abort->load(std::memory_order_acquire);
|
||||
};
|
||||
|
||||
// Phase 1: Hot key sync — enqueue hot keys first for fastest recovery
|
||||
auto hot_stats = data_manager_.value().GetHotKeyStats();
|
||||
std::unordered_set<std::string> synced_keys;
|
||||
size_t hot_count = 0;
|
||||
|
||||
for (const auto& entry : hot_stats.hot_keys) {
|
||||
if (aborted()) return;
|
||||
auto size_result = data_manager_.value().QueryObjectSize(entry.key);
|
||||
if (!size_result || size_result.value() == 0) continue;
|
||||
size_t size = size_result.value();
|
||||
auto tier_ids = data_manager_.value().GetReplicaTierIds(entry.key);
|
||||
for (const auto& tier_id : tier_ids) {
|
||||
if (notifier_) {
|
||||
// Hot keys go through normal (high-priority) queue
|
||||
if (!EnqueueWithRetry(entry.key, tier_id, size,
|
||||
/*is_hot=*/true, need_abort)) {
|
||||
return; // aborted
|
||||
}
|
||||
hot_count++;
|
||||
}
|
||||
}
|
||||
synced_keys.insert(entry.key);
|
||||
}
|
||||
LOG(INFO) << "Recovery Phase 1: enqueued " << hot_count
|
||||
<< " hot key entries";
|
||||
|
||||
if (aborted()) return;
|
||||
|
||||
// Phase 2+3: Iterate all keys in batches.
|
||||
// DRAM entries enqueued before storage entries within each batch.
|
||||
auto tier_views = data_manager_.value().GetTierViews();
|
||||
std::unordered_set<UUID, boost::hash<UUID>> dram_tiers;
|
||||
for (const auto& tv : tier_views) {
|
||||
if (tv.type == MemoryType::DRAM) {
|
||||
dram_tiers.insert(tv.id);
|
||||
}
|
||||
}
|
||||
|
||||
size_t dram_count = 0, storage_count = 0;
|
||||
bool was_aborted = false;
|
||||
|
||||
data_manager_.value().ForEachKeyBatch(
|
||||
[&](std::vector<ReplicaLocation>&& batch) -> bool {
|
||||
if (aborted()) {
|
||||
was_aborted = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pass 1: DRAM entries first for fastest recovery
|
||||
for (const auto& e : batch) {
|
||||
if (synced_keys.count(e.key)) continue;
|
||||
if (dram_tiers.count(e.tier_id)) {
|
||||
if (e.size == 0) continue;
|
||||
if (notifier_) {
|
||||
if (!EnqueueWithRetry(e.key, e.tier_id, e.size,
|
||||
/*is_hot=*/false, need_abort)) {
|
||||
was_aborted = true;
|
||||
LOG(WARNING)
|
||||
<< "fail to enqueue route recovery list";
|
||||
return false;
|
||||
}
|
||||
dram_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pass 2: Storage entries
|
||||
for (const auto& e : batch) {
|
||||
if (synced_keys.count(e.key)) continue;
|
||||
if (dram_tiers.count(e.tier_id)) continue;
|
||||
if (e.size == 0) continue;
|
||||
if (notifier_) {
|
||||
if (!EnqueueWithRetry(e.key, e.tier_id, e.size,
|
||||
/*is_hot=*/false, need_abort)) {
|
||||
was_aborted = true;
|
||||
LOG(WARNING) << "fail to enqueue route recovery list";
|
||||
return false;
|
||||
}
|
||||
storage_count++;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (was_aborted) {
|
||||
LOG(INFO) << "Recovery aborted during key iteration";
|
||||
return;
|
||||
}
|
||||
|
||||
LOG(INFO) << "Recovery enqueue complete: hot=" << hot_count
|
||||
<< ", dram=" << dram_count << ", storage=" << storage_count;
|
||||
|
||||
// Wait for recovery queue to drain (all ops sent to Master).
|
||||
static constexpr auto kRecoveryDrainTimeout = std::chrono::minutes(10);
|
||||
if (notifier_) {
|
||||
bool drained = notifier_->WaitForRecoveryDrain(
|
||||
[&]() { return aborted(); }, kRecoveryDrainTimeout);
|
||||
if (!drained) {
|
||||
if (aborted()) {
|
||||
LOG(INFO) << "Recovery aborted during drain wait";
|
||||
return;
|
||||
}
|
||||
LOG(WARNING) << "Recovery drain timed out, transitioning to FULL"
|
||||
<< " with incomplete route sync";
|
||||
}
|
||||
}
|
||||
|
||||
// All recovery routes delivered. Notify Master.
|
||||
// Retry indefinitely until success or abort — if Master restarts again,
|
||||
// HandleEvent(MASTER_UNREACHABLE) will set need_abort and this thread
|
||||
// exits.
|
||||
while (true) {
|
||||
if (aborted()) return;
|
||||
auto sync_result = SetSyncCompleted();
|
||||
if (sync_result) break;
|
||||
LOG(WARNING) << "SetSyncCompleted failed: " << sync_result.error()
|
||||
<< ", retrying in 500ms";
|
||||
std::unique_lock<std::mutex> lk(abort_mutex_);
|
||||
abort_cv_.wait_for(lk, std::chrono::milliseconds(500),
|
||||
[&] { return aborted(); });
|
||||
}
|
||||
|
||||
// Transition SYNCING→FULL if not aborted.
|
||||
if (!need_abort->load(std::memory_order_acquire)) {
|
||||
HAClientState expected = HAClientState::SYNCING;
|
||||
if (state_.compare_exchange_strong(expected, HAClientState::FULL,
|
||||
std::memory_order_acq_rel)) {
|
||||
LOG(WARNING) << "HA state: SYNCING -> FULL"
|
||||
<< ", reason=recovery complete";
|
||||
}
|
||||
}
|
||||
LOG(INFO) << "Recovery pipeline completed";
|
||||
}
|
||||
|
||||
// return false when abort
|
||||
bool HARecoveryManager::EnqueueWithRetry(const std::string& key,
|
||||
const UUID& tier_id, size_t size,
|
||||
bool is_hot,
|
||||
const AbortToken& need_abort) {
|
||||
while (true) {
|
||||
if (need_abort->load(std::memory_order_acquire)) return false;
|
||||
auto r = is_hot ? notifier_->EnqueueAdd(key, tier_id, size)
|
||||
: notifier_->EnqueueRecoveryAdd(key, tier_id, size);
|
||||
if (r) return true;
|
||||
// Queue full — yield to normal writes then retry
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
#include "default_config.h"
|
||||
#include "ha_helper.h"
|
||||
#include "http_metadata_server.h"
|
||||
#include "rpc_service.h"
|
||||
#include "centralized_rpc_service.h"
|
||||
#include "p2p_rpc_service.h"
|
||||
#include "types.h"
|
||||
|
||||
#include "master_config.h"
|
||||
|
|
@ -67,8 +68,10 @@ DEFINE_string(
|
|||
etcd_endpoints, "",
|
||||
"Endpoints of ETCD server, separated by semicolon, required in HA mode");
|
||||
DEFINE_int64(client_ttl, mooncake::DEFAULT_CLIENT_LIVE_TTL_SEC,
|
||||
"How long a client is considered alive after the last ping, only "
|
||||
"used in HA mode");
|
||||
"How long a client is considered alive after the last heartbeat");
|
||||
DEFINE_int64(
|
||||
client_crashed_ttl, mooncake::DEFAULT_CLIENT_CRASHED_TTL_SEC,
|
||||
"How long a client is considered crashed after the last heartbeat");
|
||||
|
||||
DEFINE_string(root_fs_dir, mooncake::DEFAULT_ROOT_FS_DIR,
|
||||
"Root directory for storage backend, used in HA mode");
|
||||
|
|
@ -100,6 +103,11 @@ DEFINE_bool(enable_disk_eviction, true,
|
|||
DEFINE_uint64(
|
||||
quota_bytes, 0,
|
||||
"Quota for storage backend in bytes (0 = use default 90% of capacity)");
|
||||
DEFINE_string(deployment_mode, "Centralization",
|
||||
"the deployment mode of mooncake-store, master and client must "
|
||||
"run in same mode. Options: Centralization, P2P");
|
||||
DEFINE_uint64(max_replicas_per_key, 1,
|
||||
"Maximum number of replicas per key in P2P mode (0 = no limit)");
|
||||
|
||||
void InitMasterConf(const mooncake::DefaultConfig& default_config,
|
||||
mooncake::MasterConfig& master_config) {
|
||||
|
|
@ -138,6 +146,8 @@ void InitMasterConf(const mooncake::DefaultConfig& default_config,
|
|||
default_config.GetInt64("client_live_ttl_sec",
|
||||
&master_config.client_live_ttl_sec,
|
||||
FLAGS_client_ttl);
|
||||
default_config.GetInt64("client_crashed_ttl_sec",
|
||||
&master_config.client_crashed_ttl_sec, -1);
|
||||
|
||||
default_config.GetBool("enable_ha", &master_config.enable_ha,
|
||||
FLAGS_enable_ha);
|
||||
|
|
@ -175,6 +185,11 @@ void InitMasterConf(const mooncake::DefaultConfig& default_config,
|
|||
FLAGS_enable_disk_eviction);
|
||||
default_config.GetUInt64("quota_bytes", &master_config.quota_bytes,
|
||||
FLAGS_quota_bytes);
|
||||
default_config.GetUInt64("max_replicas_per_key",
|
||||
&master_config.max_replicas_per_key,
|
||||
FLAGS_max_replicas_per_key);
|
||||
default_config.GetString("deployment_mode", &master_config.deployment_mode,
|
||||
FLAGS_deployment_mode);
|
||||
}
|
||||
|
||||
void LoadConfigFromCmdline(mooncake::MasterConfig& master_config,
|
||||
|
|
@ -298,6 +313,10 @@ void LoadConfigFromCmdline(mooncake::MasterConfig& master_config,
|
|||
!conf_set) {
|
||||
master_config.client_live_ttl_sec = FLAGS_client_ttl;
|
||||
}
|
||||
if (google::GetCommandLineFlagInfo("client_crashed_ttl", &info) &&
|
||||
!info.is_default) {
|
||||
master_config.client_crashed_ttl_sec = FLAGS_client_crashed_ttl;
|
||||
}
|
||||
if ((google::GetCommandLineFlagInfo("cluster_id", &info) &&
|
||||
!info.is_default) ||
|
||||
!conf_set) {
|
||||
|
|
@ -360,6 +379,16 @@ void LoadConfigFromCmdline(mooncake::MasterConfig& master_config,
|
|||
!conf_set) {
|
||||
master_config.quota_bytes = FLAGS_quota_bytes;
|
||||
}
|
||||
if ((google::GetCommandLineFlagInfo("deployment_mode", &info) &&
|
||||
!info.is_default) ||
|
||||
!conf_set) {
|
||||
master_config.deployment_mode = FLAGS_deployment_mode;
|
||||
}
|
||||
if ((google::GetCommandLineFlagInfo("max_replicas_per_key", &info) &&
|
||||
!info.is_default) ||
|
||||
!conf_set) {
|
||||
master_config.max_replicas_per_key = FLAGS_max_replicas_per_key;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to start HTTP metadata server
|
||||
|
|
@ -432,6 +461,24 @@ int main(int argc, char* argv[]) {
|
|||
if (value && std::string_view(value) == "rdma") {
|
||||
protocol = "rdma";
|
||||
}
|
||||
|
||||
// Process client_crashed_ttl_sec logic
|
||||
if (master_config.client_crashed_ttl_sec == -1) {
|
||||
// Not set by config file and not set by CLI -> Default to 3x live ttl
|
||||
master_config.client_crashed_ttl_sec =
|
||||
master_config.client_live_ttl_sec * 3;
|
||||
} else {
|
||||
// Explicitly set (by file or CLI) -> Validate
|
||||
if (master_config.client_crashed_ttl_sec <
|
||||
master_config.client_live_ttl_sec) {
|
||||
LOG(FATAL) << "client_crashed_ttl ("
|
||||
<< master_config.client_crashed_ttl_sec
|
||||
<< ") must be >= client_ttl ("
|
||||
<< master_config.client_live_ttl_sec << ")";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(INFO) << "Master service started on port " << master_config.rpc_port
|
||||
<< ", max_threads=" << master_config.rpc_thread_num
|
||||
<< ", enable_metric_reporting="
|
||||
|
|
@ -449,6 +496,7 @@ int main(int argc, char* argv[]) {
|
|||
<< ", enable_offload=" << master_config.enable_offload
|
||||
<< ", etcd_endpoints=" << master_config.etcd_endpoints
|
||||
<< ", client_ttl=" << master_config.client_live_ttl_sec
|
||||
<< ", client_crashed_ttl=" << master_config.client_crashed_ttl_sec
|
||||
<< ", rpc_thread_num=" << master_config.rpc_thread_num
|
||||
<< ", rpc_port=" << master_config.rpc_port
|
||||
<< ", rpc_address=" << master_config.rpc_address
|
||||
|
|
@ -471,8 +519,16 @@ int main(int argc, char* argv[]) {
|
|||
<< ", put_start_discard_timeout_sec="
|
||||
<< master_config.put_start_discard_timeout_sec
|
||||
<< ", put_start_release_timeout_sec="
|
||||
<< master_config.put_start_release_timeout_sec;
|
||||
<< master_config.put_start_release_timeout_sec
|
||||
<< ", max_replicas_per_key=" << master_config.max_replicas_per_key
|
||||
<< ", deployment_mode=" << master_config.deployment_mode;
|
||||
|
||||
if (master_config.deployment_mode != "Centralization" &&
|
||||
master_config.deployment_mode != "P2P") {
|
||||
LOG(FATAL) << "Invalid deployment mode: "
|
||||
<< master_config.deployment_mode;
|
||||
return 1;
|
||||
}
|
||||
// Start HTTP metadata server if enabled
|
||||
std::unique_ptr<mooncake::HttpMetadataServer> http_metadata_server;
|
||||
if (master_config.enable_http_metadata_server) {
|
||||
|
|
@ -505,10 +561,30 @@ int main(int argc, char* argv[]) {
|
|||
if (value && std::string_view(value) == "rdma") {
|
||||
server.init_ibv();
|
||||
}
|
||||
mooncake::WrappedMasterService wrapped_master_service(
|
||||
mooncake::WrappedMasterServiceConfig(master_config, version));
|
||||
|
||||
mooncake::RegisterRpcService(server, wrapped_master_service);
|
||||
// Declare service object outside if block to ensure it lives until
|
||||
// server.start() completes
|
||||
std::unique_ptr<mooncake::WrappedMasterService> master_service;
|
||||
|
||||
if (master_config.deployment_mode == "Centralization") {
|
||||
master_service =
|
||||
std::make_unique<mooncake::WrappedCentralizedMasterService>(
|
||||
mooncake::WrappedMasterServiceConfig(master_config,
|
||||
version));
|
||||
|
||||
mooncake::RegisterCentralizedRpcService(
|
||||
server, static_cast<mooncake::WrappedCentralizedMasterService&>(
|
||||
*master_service));
|
||||
} else {
|
||||
master_service =
|
||||
std::make_unique<mooncake::WrappedP2PMasterService>(
|
||||
mooncake::WrappedMasterServiceConfig(master_config,
|
||||
version));
|
||||
|
||||
mooncake::RegisterP2PRpcService(
|
||||
server, static_cast<mooncake::WrappedP2PMasterService&>(
|
||||
*master_service));
|
||||
}
|
||||
return server.start();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
#include "master_client.h"
|
||||
|
||||
#include <async_simple/coro/FutureAwaiter.h>
|
||||
#include <async_simple/coro/Lazy.h>
|
||||
#include <async_simple/coro/SyncAwait.h>
|
||||
|
||||
#include <coroutine>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <ylt/coro_rpc/impl/coro_rpc_client.hpp>
|
||||
|
|
@ -13,14 +10,10 @@
|
|||
#include "rpc_service.h"
|
||||
#include "types.h"
|
||||
#include "utils/scoped_vlog_timer.h"
|
||||
#include "master_metric_manager.h"
|
||||
#include "version.h"
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
template <auto Method>
|
||||
struct RpcNameTraits;
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::ExistKey> {
|
||||
static constexpr const char* value = "ExistKey";
|
||||
|
|
@ -31,11 +24,6 @@ struct RpcNameTraits<&WrappedMasterService::BatchExistKey> {
|
|||
static constexpr const char* value = "BatchExistKey";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::GetReplicaList> {
|
||||
static constexpr const char* value = "GetReplicaList";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::CalcCacheStats> {
|
||||
static constexpr const char* value = "CalcCacheStats";
|
||||
|
|
@ -46,51 +34,21 @@ struct RpcNameTraits<&WrappedMasterService::BatchQueryIp> {
|
|||
static constexpr const char* value = "BatchQueryIp";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::BatchReplicaClear> {
|
||||
static constexpr const char* value = "BatchReplicaClear";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::GetReplicaListByRegex> {
|
||||
static constexpr const char* value = "GetReplicaListByRegex";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::GetReplicaList> {
|
||||
static constexpr const char* value = "GetReplicaList";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::BatchGetReplicaList> {
|
||||
static constexpr const char* value = "BatchGetReplicaList";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::PutStart> {
|
||||
static constexpr const char* value = "PutStart";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::BatchPutStart> {
|
||||
static constexpr const char* value = "BatchPutStart";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::PutEnd> {
|
||||
static constexpr const char* value = "PutEnd";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::BatchPutEnd> {
|
||||
static constexpr const char* value = "BatchPutEnd";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::PutRevoke> {
|
||||
static constexpr const char* value = "PutRevoke";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::BatchPutRevoke> {
|
||||
static constexpr const char* value = "BatchPutRevoke";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::Remove> {
|
||||
static constexpr const char* value = "Remove";
|
||||
|
|
@ -111,29 +69,24 @@ struct RpcNameTraits<&WrappedMasterService::MountSegment> {
|
|||
static constexpr const char* value = "MountSegment";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::ReMountSegment> {
|
||||
static constexpr const char* value = "ReMountSegment";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::UnmountSegment> {
|
||||
static constexpr const char* value = "UnmountSegment";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::Ping> {
|
||||
static constexpr const char* value = "Ping";
|
||||
struct RpcNameTraits<&WrappedMasterService::Heartbeat> {
|
||||
static constexpr const char* value = "Heartbeat";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::GetFsdir> {
|
||||
static constexpr const char* value = "GetFsdir";
|
||||
struct RpcNameTraits<&WrappedMasterService::RegisterClient> {
|
||||
static constexpr const char* value = "RegisterClient";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::GetStorageConfig> {
|
||||
static constexpr const char* value = "GetStorageConfig";
|
||||
struct RpcNameTraits<&WrappedMasterService::QueryClientStatus> {
|
||||
static constexpr const char* value = "QueryClientStatus";
|
||||
};
|
||||
|
||||
template <>
|
||||
|
|
@ -141,129 +94,33 @@ struct RpcNameTraits<&WrappedMasterService::ServiceReady> {
|
|||
static constexpr const char* value = "ServiceReady";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::MountLocalDiskSegment> {
|
||||
static constexpr const char* value = "MountLocalDiskSegment";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::OffloadObjectHeartbeat> {
|
||||
static constexpr const char* value = "OffloadObjectHeartbeat";
|
||||
};
|
||||
|
||||
template <>
|
||||
struct RpcNameTraits<&WrappedMasterService::NotifyOffloadSuccess> {
|
||||
static constexpr const char* value = "NotifyOffloadSuccess";
|
||||
};
|
||||
|
||||
template <auto ServiceMethod, typename ReturnType, typename... Args>
|
||||
tl::expected<ReturnType, ErrorCode> MasterClient::invoke_rpc(Args&&... args) {
|
||||
auto pool = client_accessor_.GetClientPool();
|
||||
|
||||
// Increment RPC counter
|
||||
if (metrics_) {
|
||||
metrics_->rpc_count.inc({RpcNameTraits<ServiceMethod>::value});
|
||||
}
|
||||
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
return async_simple::coro::syncAwait(
|
||||
[&]() -> async_simple::coro::Lazy<tl::expected<ReturnType, ErrorCode>> {
|
||||
auto ret = co_await pool->send_request(
|
||||
[&](coro_io::client_reuse_hint,
|
||||
coro_rpc::coro_rpc_client& client) {
|
||||
return client.send_request<ServiceMethod>(
|
||||
std::forward<Args>(args)...);
|
||||
});
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "Client not available";
|
||||
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
|
||||
}
|
||||
auto result = co_await std::move(ret.value());
|
||||
if (!result) {
|
||||
LOG(ERROR) << "RPC call failed: " << result.error().msg;
|
||||
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
|
||||
}
|
||||
if (metrics_) {
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto latency =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
end_time - start_time);
|
||||
metrics_->rpc_latency.observe(
|
||||
{RpcNameTraits<ServiceMethod>::value}, latency.count());
|
||||
}
|
||||
co_return result->result();
|
||||
}());
|
||||
}
|
||||
|
||||
template <auto ServiceMethod, typename ResultType, typename... Args>
|
||||
std::vector<tl::expected<ResultType, ErrorCode>> MasterClient::invoke_batch_rpc(
|
||||
size_t input_size, Args&&... args) {
|
||||
auto pool = client_accessor_.GetClientPool();
|
||||
|
||||
// Increment RPC counter
|
||||
if (metrics_) {
|
||||
metrics_->rpc_count.inc({RpcNameTraits<ServiceMethod>::value});
|
||||
}
|
||||
|
||||
auto start_time = std::chrono::steady_clock::now();
|
||||
return async_simple::coro::syncAwait(
|
||||
[&]() -> async_simple::coro::Lazy<
|
||||
std::vector<tl::expected<ResultType, ErrorCode>>> {
|
||||
auto ret = co_await pool->send_request(
|
||||
[&](coro_io::client_reuse_hint,
|
||||
coro_rpc::coro_rpc_client& client) {
|
||||
return client.send_request<ServiceMethod>(
|
||||
std::forward<Args>(args)...);
|
||||
});
|
||||
if (!ret.has_value()) {
|
||||
LOG(ERROR) << "Client not available";
|
||||
co_return std::vector<tl::expected<ResultType, ErrorCode>>(
|
||||
input_size, tl::make_unexpected(ErrorCode::RPC_FAIL));
|
||||
}
|
||||
auto result = co_await std::move(ret.value());
|
||||
if (!result) {
|
||||
LOG(ERROR) << "Batch RPC call failed: " << result.error().msg;
|
||||
std::vector<tl::expected<ResultType, ErrorCode>> error_results;
|
||||
error_results.reserve(input_size);
|
||||
for (size_t i = 0; i < input_size; ++i) {
|
||||
error_results.emplace_back(
|
||||
tl::make_unexpected(ErrorCode::RPC_FAIL));
|
||||
}
|
||||
co_return error_results;
|
||||
}
|
||||
if (metrics_) {
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto latency =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
end_time - start_time);
|
||||
metrics_->rpc_latency.observe(
|
||||
{RpcNameTraits<ServiceMethod>::value}, latency.count());
|
||||
}
|
||||
co_return result->result();
|
||||
}());
|
||||
}
|
||||
|
||||
MasterClient::~MasterClient() = default;
|
||||
|
||||
ErrorCode MasterClient::Connect(const std::string& master_addr) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::Connect");
|
||||
timer.LogRequest("master_addr=", master_addr);
|
||||
|
||||
MutexLocker lock(&connect_mutex_);
|
||||
if (client_addr_param_ != master_addr) {
|
||||
bool is_same_addr = (client_addr_param_ == master_addr);
|
||||
if (!is_same_addr) {
|
||||
// WARNING: The existing client pool cannot be erased. So if there are a
|
||||
// lot of different addresses, there will be resource leak problems.
|
||||
auto client_pool = client_pools_->at(master_addr);
|
||||
client_accessor_.SetClientPool(client_pool);
|
||||
client_addr_param_ = master_addr;
|
||||
}
|
||||
auto pool = client_accessor_.GetClientPool();
|
||||
// The client pool does not have native connection check method, so we need
|
||||
// to use custom ServiceReady API.
|
||||
auto result =
|
||||
invoke_rpc<&WrappedMasterService::ServiceReady, std::string>();
|
||||
if (!result.has_value() && is_same_addr) {
|
||||
timer.LogResponse("error_code=", result.error());
|
||||
// Stale connection pool might still exist.
|
||||
// Retrying once will force the pool to re-establish a new connection.
|
||||
result = invoke_rpc<&WrappedMasterService::ServiceReady, std::string>();
|
||||
}
|
||||
|
||||
if (!result.has_value()) {
|
||||
timer.LogResponse("error_code=", result.error());
|
||||
client_addr_param_.clear();
|
||||
return result.error();
|
||||
}
|
||||
// Check if server version matches client version
|
||||
|
|
@ -300,6 +157,46 @@ std::vector<tl::expected<bool, ErrorCode>> MasterClient::BatchExistKey(
|
|||
return result;
|
||||
}
|
||||
|
||||
tl::expected<GetReplicaListResponse, ErrorCode> MasterClient::GetReplicaList(
|
||||
const std::string& key, const GetReplicaListRequestConfig& config) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::GetReplicaList");
|
||||
timer.LogRequest("object_key=", key);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::GetReplicaList,
|
||||
GetReplicaListResponse>(key, config);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async_simple::coro::Lazy<tl::expected<GetReplicaListResponse, ErrorCode>>
|
||||
MasterClient::AsyncGetReplicaList(const std::string& key,
|
||||
const GetReplicaListRequestConfig& config) {
|
||||
auto result =
|
||||
co_await invoke_rpc_async<&WrappedMasterService::GetReplicaList,
|
||||
GetReplicaListResponse>(key, config);
|
||||
co_return result;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
|
||||
MasterClient::BatchGetReplicaList(const std::vector<std::string>& keys,
|
||||
const GetReplicaListRequestConfig& config) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::BatchGetReplicaList");
|
||||
timer.LogRequest("requests_count=", keys.size());
|
||||
|
||||
if (keys.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto result = invoke_rpc<
|
||||
&WrappedMasterService::BatchGetReplicaList,
|
||||
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>>(keys,
|
||||
config);
|
||||
if (result.has_value()) {
|
||||
timer.LogResponse("result=", result.value().size(), " requests");
|
||||
}
|
||||
return result.value();
|
||||
}
|
||||
|
||||
tl::expected<MasterMetricManager::CacheHitStatDict, ErrorCode>
|
||||
MasterClient::CalcCacheStats() {
|
||||
return invoke_rpc<&WrappedMasterService::CalcCacheStats,
|
||||
|
|
@ -322,21 +219,6 @@ MasterClient::BatchQueryIp(const std::vector<UUID>& client_ids) {
|
|||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode>
|
||||
MasterClient::BatchReplicaClear(const std::vector<std::string>& object_keys,
|
||||
const UUID& client_id,
|
||||
const std::string& segment_name) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::BatchReplicaClear");
|
||||
timer.LogRequest("object_keys_count=", object_keys.size(),
|
||||
", client_id=", client_id,
|
||||
", segment_name=", segment_name);
|
||||
auto result = invoke_rpc<&WrappedMasterService::BatchReplicaClear,
|
||||
std::vector<std::string>>(object_keys, client_id,
|
||||
segment_name);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
|
||||
ErrorCode>
|
||||
MasterClient::GetReplicaListByRegex(const std::string& str) {
|
||||
|
|
@ -351,117 +233,6 @@ MasterClient::GetReplicaListByRegex(const std::string& str) {
|
|||
return result;
|
||||
}
|
||||
|
||||
tl::expected<GetReplicaListResponse, ErrorCode> MasterClient::GetReplicaList(
|
||||
const std::string& object_key) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::GetReplicaList");
|
||||
timer.LogRequest("object_key=", object_key);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::GetReplicaList,
|
||||
GetReplicaListResponse>(object_key);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
|
||||
MasterClient::BatchGetReplicaList(const std::vector<std::string>& object_keys) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::BatchGetReplicaList");
|
||||
timer.LogRequest("keys_count=", object_keys.size());
|
||||
|
||||
auto result = invoke_batch_rpc<&WrappedMasterService::BatchGetReplicaList,
|
||||
GetReplicaListResponse>(object_keys.size(),
|
||||
object_keys);
|
||||
timer.LogResponse("result=", result.size(), " operations");
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
|
||||
MasterClient::PutStart(const std::string& key,
|
||||
const std::vector<size_t>& slice_lengths,
|
||||
const ReplicateConfig& config) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::PutStart");
|
||||
timer.LogRequest("key=", key, ", slice_count=", slice_lengths.size());
|
||||
|
||||
uint64_t total_slice_length = 0;
|
||||
for (const auto& slice_length : slice_lengths) {
|
||||
total_slice_length += slice_length;
|
||||
}
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::PutStart,
|
||||
std::vector<Replica::Descriptor>>(
|
||||
client_id_, key, total_slice_length, config);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
||||
MasterClient::BatchPutStart(
|
||||
const std::vector<std::string>& keys,
|
||||
const std::vector<std::vector<uint64_t>>& slice_lengths,
|
||||
const ReplicateConfig& config) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::BatchPutStart");
|
||||
timer.LogRequest("keys_count=", keys.size());
|
||||
|
||||
std::vector<uint64_t> total_slice_lengths;
|
||||
total_slice_lengths.reserve(slice_lengths.size());
|
||||
for (const auto& slice_lengths : slice_lengths) {
|
||||
uint64_t total_slice_length = 0;
|
||||
for (const auto& slice_length : slice_lengths) {
|
||||
total_slice_length += slice_length;
|
||||
}
|
||||
total_slice_lengths.emplace_back(total_slice_length);
|
||||
}
|
||||
|
||||
auto result = invoke_batch_rpc<&WrappedMasterService::BatchPutStart,
|
||||
std::vector<Replica::Descriptor>>(
|
||||
keys.size(), client_id_, keys, total_slice_lengths, config);
|
||||
timer.LogResponse("result=", result.size(), " operations");
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> MasterClient::PutEnd(const std::string& key,
|
||||
ReplicaType replica_type) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::PutEnd");
|
||||
timer.LogRequest("key=", key);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::PutEnd, void>(
|
||||
client_id_, key, replica_type);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> MasterClient::BatchPutEnd(
|
||||
const std::vector<std::string>& keys) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::BatchPutEnd");
|
||||
timer.LogRequest("keys_count=", keys.size());
|
||||
|
||||
auto result = invoke_batch_rpc<&WrappedMasterService::BatchPutEnd, void>(
|
||||
keys.size(), client_id_, keys);
|
||||
timer.LogResponse("result=", result.size(), " operations");
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> MasterClient::PutRevoke(
|
||||
const std::string& key, ReplicaType replica_type) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::PutRevoke");
|
||||
timer.LogRequest("key=", key);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::PutRevoke, void>(
|
||||
client_id_, key, replica_type);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<tl::expected<void, ErrorCode>> MasterClient::BatchPutRevoke(
|
||||
const std::vector<std::string>& keys) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::BatchPutRevoke");
|
||||
timer.LogRequest("keys_count=", keys.size());
|
||||
|
||||
auto result = invoke_batch_rpc<&WrappedMasterService::BatchPutRevoke, void>(
|
||||
keys.size(), client_id_, keys);
|
||||
timer.LogResponse("result=", result.size(), " operations");
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> MasterClient::Remove(const std::string& key) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::Remove");
|
||||
timer.LogRequest("key=", key);
|
||||
|
|
@ -490,31 +261,6 @@ tl::expected<long, ErrorCode> MasterClient::RemoveAll() {
|
|||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> MasterClient::MountSegment(
|
||||
const Segment& segment) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::MountSegment");
|
||||
timer.LogRequest("base=", segment.base, ", size=", segment.size,
|
||||
", name=", segment.name, ", id=", segment.id,
|
||||
", client_id=", client_id_);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::MountSegment, void>(
|
||||
segment, client_id_);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> MasterClient::ReMountSegment(
|
||||
const std::vector<Segment>& segments) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::ReMountSegment");
|
||||
timer.LogRequest("segments_num=", segments.size(),
|
||||
", client_id=", client_id_);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::ReMountSegment, void>(
|
||||
segments, client_id_);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> MasterClient::UnmountSegment(
|
||||
const UUID& segment_id) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::UnmountSegment");
|
||||
|
|
@ -526,73 +272,53 @@ tl::expected<void, ErrorCode> MasterClient::UnmountSegment(
|
|||
return result;
|
||||
}
|
||||
|
||||
tl::expected<PingResponse, ErrorCode> MasterClient::Ping() {
|
||||
ScopedVLogTimer timer(1, "MasterClient::Ping");
|
||||
tl::expected<HeartbeatResponse, ErrorCode> MasterClient::Heartbeat(
|
||||
const HeartbeatRequest& req) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::Heartbeat");
|
||||
timer.LogRequest("client_id=", client_id_);
|
||||
|
||||
auto result =
|
||||
invoke_rpc<&WrappedMasterService::Ping, PingResponse>(client_id_);
|
||||
invoke_rpc<&WrappedMasterService::Heartbeat, HeartbeatResponse>(req);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::string, ErrorCode> MasterClient::GetFsdir() {
|
||||
ScopedVLogTimer timer(1, "MasterClient::GetFsdir");
|
||||
timer.LogRequest("action=get_fsdir");
|
||||
tl::expected<QueryClientStatusResponse, ErrorCode>
|
||||
MasterClient::QueryClientStatus(const UUID& client_id) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::QueryClientStatus");
|
||||
timer.LogRequest("client_id=", client_id);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::GetFsdir, std::string>();
|
||||
QueryClientStatusRequest req;
|
||||
req.client_id = client_id;
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::QueryClientStatus,
|
||||
QueryClientStatusResponse>(req);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<GetStorageConfigResponse, ErrorCode>
|
||||
MasterClient::GetStorageConfig() {
|
||||
ScopedVLogTimer timer(1, "MasterClient::GetStorageConfig");
|
||||
timer.LogRequest("action=get_storage_config");
|
||||
tl::expected<void, ErrorCode> MasterClient::MountSegment(
|
||||
const Segment& segment) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::MountSegment");
|
||||
timer.LogRequest("segment_name=", segment.name, ", client_id=", client_id_);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::GetStorageConfig,
|
||||
GetStorageConfigResponse>();
|
||||
auto result = invoke_rpc<&WrappedMasterService::MountSegment, void>(
|
||||
segment, client_id_);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> MasterClient::MountLocalDiskSegment(
|
||||
const UUID& client_id, bool enable_offloading) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::MountLocalDiskSegment");
|
||||
timer.LogRequest("client_id=", client_id,
|
||||
", enable_offloading=", enable_offloading);
|
||||
tl::expected<RegisterClientResponse, ErrorCode> MasterClient::RegisterClient(
|
||||
const RegisterClientRequest& req) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::RegisterClient");
|
||||
timer.LogRequest("client_id=", client_id_,
|
||||
", segments_count=", req.segments.size(),
|
||||
", deployment_mode=", req.deployment_mode);
|
||||
|
||||
auto result =
|
||||
invoke_rpc<&WrappedMasterService::MountLocalDiskSegment, void>(
|
||||
client_id, enable_offloading);
|
||||
auto result = invoke_rpc<&WrappedMasterService::RegisterClient,
|
||||
RegisterClientResponse>(req);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>
|
||||
MasterClient::OffloadObjectHeartbeat(const UUID& client_id,
|
||||
bool enable_offloading) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::OffloadObjectHeartbeat");
|
||||
timer.LogRequest("client_id=", client_id,
|
||||
", enable_offloading=", enable_offloading);
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::OffloadObjectHeartbeat,
|
||||
std::unordered_map<std::string, int64_t>>(
|
||||
client_id, enable_offloading);
|
||||
return result;
|
||||
}
|
||||
|
||||
tl::expected<void, ErrorCode> MasterClient::NotifyOffloadSuccess(
|
||||
const UUID& client_id, const std::vector<std::string>& keys,
|
||||
const std::vector<StorageObjectMetadata>& metadatas) {
|
||||
ScopedVLogTimer timer(1, "MasterClient::NotifyOffloadSuccess");
|
||||
timer.LogRequest("client_id=", client_id, ", keys_count=", keys.size(),
|
||||
", metadatas_count=", metadatas.size());
|
||||
|
||||
auto result = invoke_rpc<&WrappedMasterService::NotifyOffloadSuccess, void>(
|
||||
client_id, keys, metadatas);
|
||||
timer.LogResponseExpected(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
} // namespace mooncake
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
#include <sstream> // For string building during serialization
|
||||
#include <vector> // Required by histogram serialization
|
||||
#include <cmath>
|
||||
#include <new>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
|
|
@ -109,10 +110,24 @@ MasterMetricManager::MasterMetricManager()
|
|||
remount_segment_failures_(
|
||||
"master_remount_segment_failures_total",
|
||||
"Total number of failed RemountSegment requests"),
|
||||
ping_requests_("master_ping_requests_total",
|
||||
"Total number of ping requests received"),
|
||||
ping_failures_("master_ping_failures_total",
|
||||
"Total number of failed ping requests"),
|
||||
heartbeat_requests_("master_heartbeat_requests_total",
|
||||
"Total number of heartbeat requests received"),
|
||||
heartbeat_failures_("master_heartbeat_failures_total",
|
||||
"Total number of failed heartbeat requests"),
|
||||
get_write_route_requests_("master_get_write_route_requests_total",
|
||||
"Total number of get write route requests"),
|
||||
get_write_route_failures_(
|
||||
"master_get_write_route_failures_total",
|
||||
"Total number of failed get write route requests"),
|
||||
add_replica_requests_("master_add_replica_requests_total",
|
||||
"Total number of add replica requests"),
|
||||
add_replica_failures_("master_add_replica_failures_total",
|
||||
"Total number of failed add replica requests"),
|
||||
remove_replica_requests_("master_remove_replica_requests_total",
|
||||
"Total number of remove replica requests"),
|
||||
remove_replica_failures_(
|
||||
"master_remove_replica_failures_total",
|
||||
"Total number of failed remove replica requests"),
|
||||
|
||||
// Initialize Batch Request Counters
|
||||
batch_exist_key_requests_(
|
||||
|
|
@ -217,6 +232,30 @@ MasterMetricManager::MasterMetricManager()
|
|||
batch_put_revoke_failed_items_(
|
||||
"master_batch_put_revoke_failed_items_total",
|
||||
"Total number of failed items in BatchPutRevoke requests"),
|
||||
batch_remove_replica_requests_(
|
||||
"master_batch_remove_replica_requests_total",
|
||||
"Total number of BatchRemoveReplica requests received"),
|
||||
batch_remove_replica_failures_(
|
||||
"master_batch_remove_replica_failures_total",
|
||||
"Total number of failed BatchRemoveReplica requests"),
|
||||
batch_remove_replica_partial_successes_(
|
||||
"master_batch_remove_replica_partial_successes_total",
|
||||
"Total number of partially successful BatchRemoveReplica requests"),
|
||||
batch_remove_replica_items_(
|
||||
"master_batch_remove_replica_items_total",
|
||||
"Total number of items processed in BatchRemoveReplica requests"),
|
||||
batch_remove_replica_failed_items_(
|
||||
"master_batch_remove_replica_failed_items_total",
|
||||
"Total number of failed items in BatchRemoveReplica requests"),
|
||||
batch_get_write_route_requests_(
|
||||
"master_batch_get_write_route_requests_total",
|
||||
"Total number of BatchGetWriteRoute requests received"),
|
||||
batch_get_write_route_failures_(
|
||||
"master_batch_get_write_route_failures_total",
|
||||
"Total number of failed BatchGetWriteRoute requests"),
|
||||
batch_get_write_route_partial_successes_(
|
||||
"master_batch_get_write_route_partial_successes_total",
|
||||
"Total number of partially successful BatchGetWriteRoute requests"),
|
||||
|
||||
// Initialize cache hit rate metrics
|
||||
mem_cache_hit_nums_("mem_cache_hit_nums_",
|
||||
|
|
@ -250,10 +289,14 @@ MasterMetricManager::MasterMetricManager()
|
|||
"master_put_start_discarded_staging_size",
|
||||
"Total size of memory replicas in discarded but not yet released "
|
||||
"PutStart operations") {
|
||||
// Update all metrics once to ensure zero values are serialized
|
||||
update_metrics_for_zero_output();
|
||||
}
|
||||
|
||||
void MasterMetricManager::reset_all_metrics() {
|
||||
this->~MasterMetricManager();
|
||||
new (this) MasterMetricManager();
|
||||
}
|
||||
|
||||
// --- Metric Interface Methods ---
|
||||
|
||||
void MasterMetricManager::update_metrics_for_zero_output() {
|
||||
|
|
@ -294,8 +337,14 @@ void MasterMetricManager::update_metrics_for_zero_output() {
|
|||
unmount_segment_failures_.inc(0);
|
||||
remount_segment_requests_.inc(0);
|
||||
remount_segment_failures_.inc(0);
|
||||
ping_requests_.inc(0);
|
||||
ping_failures_.inc(0);
|
||||
heartbeat_requests_.inc(0);
|
||||
heartbeat_failures_.inc(0);
|
||||
get_write_route_requests_.inc(0);
|
||||
get_write_route_failures_.inc(0);
|
||||
add_replica_requests_.inc(0);
|
||||
add_replica_failures_.inc(0);
|
||||
remove_replica_requests_.inc(0);
|
||||
remove_replica_failures_.inc(0);
|
||||
|
||||
// Update Batch Request Counters
|
||||
batch_exist_key_requests_.inc(0);
|
||||
|
|
@ -333,6 +382,14 @@ void MasterMetricManager::update_metrics_for_zero_output() {
|
|||
batch_put_revoke_partial_successes_.inc(0);
|
||||
batch_put_revoke_items_.inc(0);
|
||||
batch_put_revoke_failed_items_.inc(0);
|
||||
batch_remove_replica_requests_.inc(0);
|
||||
batch_remove_replica_failures_.inc(0);
|
||||
batch_remove_replica_partial_successes_.inc(0);
|
||||
batch_remove_replica_items_.inc(0);
|
||||
batch_remove_replica_failed_items_.inc(0);
|
||||
batch_get_write_route_requests_.inc(0);
|
||||
batch_get_write_route_failures_.inc(0);
|
||||
batch_get_write_route_partial_successes_.inc(0);
|
||||
|
||||
// Update cache hit rate metrics
|
||||
mem_cache_hit_nums_.inc(0);
|
||||
|
|
@ -594,11 +651,29 @@ void MasterMetricManager::inc_remount_segment_requests(int64_t val) {
|
|||
void MasterMetricManager::inc_remount_segment_failures(int64_t val) {
|
||||
remount_segment_failures_.inc(val);
|
||||
}
|
||||
void MasterMetricManager::inc_ping_requests(int64_t val) {
|
||||
ping_requests_.inc(val);
|
||||
void MasterMetricManager::inc_heartbeat_requests(int64_t val) {
|
||||
heartbeat_requests_.inc(val);
|
||||
}
|
||||
void MasterMetricManager::inc_ping_failures(int64_t val) {
|
||||
ping_failures_.inc(val);
|
||||
void MasterMetricManager::inc_heartbeat_failures(int64_t val) {
|
||||
heartbeat_failures_.inc(val);
|
||||
}
|
||||
void MasterMetricManager::inc_get_write_route_requests(int64_t val) {
|
||||
get_write_route_requests_.inc(val);
|
||||
}
|
||||
void MasterMetricManager::inc_get_write_route_failures(int64_t val) {
|
||||
get_write_route_failures_.inc(val);
|
||||
}
|
||||
void MasterMetricManager::inc_add_replica_requests(int64_t val) {
|
||||
add_replica_requests_.inc(val);
|
||||
}
|
||||
void MasterMetricManager::inc_add_replica_failures(int64_t val) {
|
||||
add_replica_failures_.inc(val);
|
||||
}
|
||||
void MasterMetricManager::inc_remove_replica_requests(int64_t val) {
|
||||
remove_replica_requests_.inc(val);
|
||||
}
|
||||
void MasterMetricManager::inc_remove_replica_failures(int64_t val) {
|
||||
remove_replica_failures_.inc(val);
|
||||
}
|
||||
|
||||
// Batch Operation Statistics (Counters)
|
||||
|
|
@ -695,6 +770,34 @@ void MasterMetricManager::inc_batch_put_revoke_partial_success(
|
|||
batch_put_revoke_partial_successes_.inc(1);
|
||||
batch_put_revoke_failed_items_.inc(failed_items);
|
||||
}
|
||||
void MasterMetricManager::inc_batch_remove_replica_requests(int64_t items) {
|
||||
batch_remove_replica_requests_.inc(1);
|
||||
batch_remove_replica_items_.inc(items);
|
||||
}
|
||||
void MasterMetricManager::inc_batch_remove_replica_failures(
|
||||
int64_t failed_items) {
|
||||
batch_remove_replica_failures_.inc(1);
|
||||
batch_remove_replica_failed_items_.inc(failed_items);
|
||||
}
|
||||
void MasterMetricManager::inc_batch_remove_replica_partial_success(
|
||||
int64_t failed_items) {
|
||||
batch_remove_replica_partial_successes_.inc(1);
|
||||
batch_remove_replica_failed_items_.inc(failed_items);
|
||||
}
|
||||
void MasterMetricManager::inc_batch_get_write_route_requests(int64_t items) {
|
||||
batch_get_write_route_requests_.inc(1);
|
||||
(void)items;
|
||||
}
|
||||
void MasterMetricManager::inc_batch_get_write_route_failures(
|
||||
int64_t failed_items) {
|
||||
batch_get_write_route_failures_.inc(1);
|
||||
(void)failed_items;
|
||||
}
|
||||
void MasterMetricManager::inc_batch_get_write_route_partial_success(
|
||||
int64_t failed_items) {
|
||||
batch_get_write_route_partial_successes_.inc(1);
|
||||
(void)failed_items;
|
||||
}
|
||||
|
||||
// PutStart Discard Metrics
|
||||
void MasterMetricManager::inc_put_start_discard_cnt(int64_t count,
|
||||
|
|
@ -797,6 +900,25 @@ int64_t MasterMetricManager::get_unmount_segment_failures() {
|
|||
return unmount_segment_failures_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_get_write_route_requests() {
|
||||
return get_write_route_requests_.value();
|
||||
}
|
||||
int64_t MasterMetricManager::get_get_write_route_failures() {
|
||||
return get_write_route_failures_.value();
|
||||
}
|
||||
int64_t MasterMetricManager::get_add_replica_requests() {
|
||||
return add_replica_requests_.value();
|
||||
}
|
||||
int64_t MasterMetricManager::get_add_replica_failures() {
|
||||
return add_replica_failures_.value();
|
||||
}
|
||||
int64_t MasterMetricManager::get_remove_replica_requests() {
|
||||
return remove_replica_requests_.value();
|
||||
}
|
||||
int64_t MasterMetricManager::get_remove_replica_failures() {
|
||||
return remove_replica_failures_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_remount_segment_requests() {
|
||||
return remount_segment_requests_.value();
|
||||
}
|
||||
|
|
@ -805,12 +927,12 @@ int64_t MasterMetricManager::get_remount_segment_failures() {
|
|||
return remount_segment_failures_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_ping_requests() {
|
||||
return ping_requests_.value();
|
||||
int64_t MasterMetricManager::get_heartbeat_requests() {
|
||||
return heartbeat_requests_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_ping_failures() {
|
||||
return ping_failures_.value();
|
||||
int64_t MasterMetricManager::get_heartbeat_failures() {
|
||||
return heartbeat_failures_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_exist_key_requests() {
|
||||
|
|
@ -953,6 +1075,38 @@ int64_t MasterMetricManager::get_batch_put_revoke_failed_items() {
|
|||
return batch_put_revoke_failed_items_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_remove_replica_requests() {
|
||||
return batch_remove_replica_requests_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_remove_replica_failures() {
|
||||
return batch_remove_replica_failures_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_remove_replica_partial_successes() {
|
||||
return batch_remove_replica_partial_successes_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_remove_replica_items() {
|
||||
return batch_remove_replica_items_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_remove_replica_failed_items() {
|
||||
return batch_remove_replica_failed_items_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_get_write_route_requests() {
|
||||
return batch_get_write_route_requests_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_get_write_route_failures() {
|
||||
return batch_get_write_route_failures_.value();
|
||||
}
|
||||
|
||||
int64_t MasterMetricManager::get_batch_get_write_route_partial_successes() {
|
||||
return batch_get_write_route_partial_successes_.value();
|
||||
}
|
||||
|
||||
// Eviction Metrics
|
||||
void MasterMetricManager::inc_eviction_success(int64_t key_count,
|
||||
int64_t size) {
|
||||
|
|
@ -1046,8 +1200,14 @@ std::string MasterMetricManager::serialize_metrics() {
|
|||
serialize_metric(unmount_segment_failures_);
|
||||
serialize_metric(remount_segment_requests_);
|
||||
serialize_metric(remount_segment_failures_);
|
||||
serialize_metric(ping_requests_);
|
||||
serialize_metric(ping_failures_);
|
||||
serialize_metric(heartbeat_requests_);
|
||||
serialize_metric(heartbeat_failures_);
|
||||
serialize_metric(get_write_route_requests_);
|
||||
serialize_metric(get_write_route_failures_);
|
||||
serialize_metric(add_replica_requests_);
|
||||
serialize_metric(add_replica_failures_);
|
||||
serialize_metric(remove_replica_requests_);
|
||||
serialize_metric(remove_replica_failures_);
|
||||
|
||||
// Serialize Batch Request Counters
|
||||
serialize_metric(batch_exist_key_requests_);
|
||||
|
|
@ -1058,12 +1218,23 @@ std::string MasterMetricManager::serialize_metrics() {
|
|||
serialize_metric(batch_replica_clear_failures_);
|
||||
serialize_metric(batch_get_replica_list_requests_);
|
||||
serialize_metric(batch_get_replica_list_failures_);
|
||||
serialize_metric(batch_get_replica_list_partial_successes_);
|
||||
serialize_metric(batch_get_replica_list_items_);
|
||||
serialize_metric(batch_get_replica_list_failed_items_);
|
||||
serialize_metric(batch_put_start_requests_);
|
||||
serialize_metric(batch_put_start_failures_);
|
||||
serialize_metric(batch_put_end_requests_);
|
||||
serialize_metric(batch_put_end_failures_);
|
||||
serialize_metric(batch_put_revoke_requests_);
|
||||
serialize_metric(batch_put_revoke_failures_);
|
||||
serialize_metric(batch_remove_replica_requests_);
|
||||
serialize_metric(batch_remove_replica_failures_);
|
||||
serialize_metric(batch_remove_replica_partial_successes_);
|
||||
serialize_metric(batch_remove_replica_items_);
|
||||
serialize_metric(batch_remove_replica_failed_items_);
|
||||
serialize_metric(batch_get_write_route_requests_);
|
||||
serialize_metric(batch_get_write_route_failures_);
|
||||
serialize_metric(batch_get_write_route_partial_successes_);
|
||||
|
||||
// Serialize Eviction Counters
|
||||
serialize_metric(eviction_success_);
|
||||
|
|
@ -1223,6 +1394,14 @@ std::string MasterMetricManager::get_summary_string() {
|
|||
int64_t batch_replica_clear_items = batch_replica_clear_items_.value();
|
||||
int64_t batch_replica_clear_failed_items =
|
||||
batch_replica_clear_failed_items_.value();
|
||||
int64_t batch_remove_replica_requests =
|
||||
batch_remove_replica_requests_.value();
|
||||
int64_t batch_remove_replica_fails = batch_remove_replica_failures_.value();
|
||||
int64_t batch_remove_replica_partial_successes =
|
||||
batch_remove_replica_partial_successes_.value();
|
||||
int64_t batch_remove_replica_items = batch_remove_replica_items_.value();
|
||||
int64_t batch_remove_replica_failed_items =
|
||||
batch_remove_replica_failed_items_.value();
|
||||
|
||||
// Eviction counters
|
||||
int64_t eviction_success = eviction_success_.value();
|
||||
|
|
@ -1230,9 +1409,9 @@ std::string MasterMetricManager::get_summary_string() {
|
|||
int64_t evicted_key_count = evicted_key_count_.value();
|
||||
int64_t evicted_size = evicted_size_.value();
|
||||
|
||||
// Ping counters
|
||||
int64_t ping = ping_requests_.value();
|
||||
int64_t ping_fails = ping_failures_.value();
|
||||
// Heartbeat counters
|
||||
int64_t heartbeat = heartbeat_requests_.value();
|
||||
int64_t heartbeat_fails = heartbeat_failures_.value();
|
||||
|
||||
// Discard counters
|
||||
int64_t put_start_discard_cnt = put_start_discard_cnt_.value();
|
||||
|
|
@ -1265,7 +1444,8 @@ std::string MasterMetricManager::get_summary_string() {
|
|||
ss << "Del=" << removes - remove_fails << "/" << removes << ", ";
|
||||
ss << "DelAll=" << remove_all - remove_all_fails << "/" << remove_all
|
||||
<< ", ";
|
||||
ss << "Ping=" << ping - ping_fails << "/" << ping << ", ";
|
||||
ss << "Heartbeat=" << heartbeat - heartbeat_fails << "/" << heartbeat
|
||||
<< ", ";
|
||||
|
||||
// Batch request summary
|
||||
ss << " | Batch Requests "
|
||||
|
|
@ -1319,14 +1499,24 @@ std::string MasterMetricManager::get_summary_string() {
|
|||
<< batch_replica_clear_requests << ", Item="
|
||||
<< batch_replica_clear_items - batch_replica_clear_failed_items << "/"
|
||||
<< batch_replica_clear_items << "), ";
|
||||
ss << "RemoveRep:(Req="
|
||||
<< batch_remove_replica_requests - batch_remove_replica_fails -
|
||||
batch_remove_replica_partial_successes
|
||||
<< "/" << batch_remove_replica_partial_successes << "/"
|
||||
<< batch_remove_replica_requests << ", Item="
|
||||
<< batch_remove_replica_items - batch_remove_replica_failed_items << "/"
|
||||
<< batch_remove_replica_items << "), ";
|
||||
|
||||
// Eviction summary
|
||||
ss << " | Eviction: " << "Success/Attempts=" << eviction_success << "/"
|
||||
<< eviction_attempts << ", " << "keys=" << evicted_key_count << ", "
|
||||
ss << " | Eviction: "
|
||||
<< "Success/Attempts=" << eviction_success << "/" << eviction_attempts
|
||||
<< ", "
|
||||
<< "keys=" << evicted_key_count << ", "
|
||||
<< "size=" << byte_size_to_string(evicted_size);
|
||||
|
||||
// Discard summary
|
||||
ss << " | Discard: " << "Released/Total=" << put_start_release_cnt << "/"
|
||||
ss << " | Discard: "
|
||||
<< "Released/Total=" << put_start_release_cnt << "/"
|
||||
<< put_start_discard_cnt << ", StagingSize="
|
||||
<< byte_size_to_string(put_start_discarded_staging_size);
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,95 @@
|
|||
#include "p2p_client_manager.h"
|
||||
#include "p2p_client_meta.h"
|
||||
#include <glog/logging.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace mooncake {
|
||||
|
||||
class CapacityPriorityIterator : public ClientIterator {
|
||||
public:
|
||||
CapacityPriorityIterator(
|
||||
const std::unordered_map<UUID, std::shared_ptr<ClientMeta>,
|
||||
boost::hash<UUID>>& client_metas) {
|
||||
if (client_metas.empty()) return;
|
||||
|
||||
clients_.reserve(client_metas.size());
|
||||
for (auto& client : client_metas) {
|
||||
if (auto p2p_meta =
|
||||
std::static_pointer_cast<P2PClientMeta>(client.second)) {
|
||||
clients_.emplace_back(p2p_meta);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(clients_.begin(), clients_.end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return std::static_pointer_cast<P2PClientMeta>(a)
|
||||
->GetAvailableCapacity() >
|
||||
std::static_pointer_cast<P2PClientMeta>(b)
|
||||
->GetAvailableCapacity();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
P2PClientManager::P2PClientManager(const int64_t disconnect_timeout_sec,
|
||||
const int64_t crash_timeout_sec,
|
||||
const ViewVersionId view_version)
|
||||
: ClientManager(disconnect_timeout_sec, crash_timeout_sec, view_version) {}
|
||||
|
||||
std::unique_ptr<ClientIterator> P2PClientManager::InnerBuildClientIterator(
|
||||
ObjectIterateStrategy strategy) {
|
||||
auto iterator = ClientManager::InnerBuildClientIterator(strategy);
|
||||
if (iterator) {
|
||||
return iterator;
|
||||
}
|
||||
switch (strategy) {
|
||||
case ObjectIterateStrategy::CAPACITY_PRIORITY:
|
||||
return std::make_unique<CapacityPriorityIterator>(client_metas_);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<ClientMeta> P2PClientManager::CreateClientMeta(
|
||||
const RegisterClientRequest& req) {
|
||||
auto meta = std::make_shared<P2PClientMeta>(
|
||||
req.client_id, req.ip_address.value_or(""), req.rpc_port.value_or(0));
|
||||
return meta;
|
||||
}
|
||||
|
||||
HeartbeatTaskResult P2PClientManager::ProcessTask(const UUID& client_id,
|
||||
const HeartbeatTask& task) {
|
||||
HeartbeatTaskResult result;
|
||||
result.type = task.type_;
|
||||
|
||||
switch (task.type_) {
|
||||
case HeartbeatTaskType::SYNC_SEGMENT_META: {
|
||||
auto client_meta =
|
||||
std::static_pointer_cast<P2PClientMeta>(GetClient(client_id));
|
||||
const auto* param = std::get_if<SyncSegmentMetaParam>(&task.param_);
|
||||
if (client_meta && param) {
|
||||
auto sync_res =
|
||||
client_meta->UpdateSegmentUsages(param->tier_usages);
|
||||
result.detail = sync_res;
|
||||
for (const auto& sub : sync_res.sub_results) {
|
||||
if (sub.error != ErrorCode::OK) {
|
||||
// result.error means the task is failed.
|
||||
// here just sub task error, don't affect task result.
|
||||
LOG(ERROR) << "fail to update segment usages"
|
||||
<< ", client_id=" << client_id
|
||||
<< ", segment_id=" << sub.segment_id
|
||||
<< ", error=" << sub.error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.error = ErrorCode::INVALID_PARAMS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
result.error = ErrorCode::NOT_IMPLEMENTED;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
#include "p2p_client_meta.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <glog/logging.h>
|
||||
|
||||
namespace mooncake {
|
||||
P2PClientMeta::P2PClientMeta(const UUID& client_id,
|
||||
const std::string& ip_address, uint16_t rpc_port)
|
||||
: ClientMeta(client_id), ip_address_(ip_address), rpc_port_(rpc_port) {
|
||||
segment_manager_ = std::make_shared<P2PSegmentManager>();
|
||||
segment_manager_->SetSegmentChangeCallbacks(
|
||||
[this](const Segment& segment) {
|
||||
// OnSegmentAddedCallback
|
||||
SpinRWLockLocker lock(&capacity_mutex_);
|
||||
client_capacity_ += segment.size;
|
||||
client_usage_ += segment.GetP2PExtra().usage;
|
||||
},
|
||||
[this](const Segment& segment) {
|
||||
// OnSegmentRemovedCallback
|
||||
SpinRWLockLocker lock(&capacity_mutex_);
|
||||
client_capacity_ -= segment.size;
|
||||
client_usage_ -= segment.GetP2PExtra().usage;
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<SegmentManager> P2PClientMeta::GetSegmentManager() {
|
||||
return segment_manager_;
|
||||
}
|
||||
|
||||
tl::expected<std::vector<std::string>, ErrorCode> P2PClientMeta::QueryIp(
|
||||
const UUID& client_id) {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(ERROR) << "fail to inner check client status"
|
||||
<< ", client_id=" << client_id_
|
||||
<< ", ret=" << check_ret.error();
|
||||
return tl::make_unexpected(check_ret.error());
|
||||
}
|
||||
return std::vector<std::string>{ip_address_};
|
||||
}
|
||||
|
||||
SyncSegmentMetaResult P2PClientMeta::UpdateSegmentUsages(
|
||||
const std::vector<TierUsageInfo>& usages) {
|
||||
SyncSegmentMetaResult result;
|
||||
SpinRWLockLocker lock(&capacity_mutex_);
|
||||
for (const auto& usage : usages) {
|
||||
SyncSegmentMetaResult::SubResult sub_res;
|
||||
sub_res.segment_id = usage.segment_id;
|
||||
|
||||
auto old_usage =
|
||||
segment_manager_->UpdateSegmentUsage(usage.segment_id, usage.usage);
|
||||
if (!old_usage.has_value()) {
|
||||
LOG(WARNING) << "fail to update segment usage"
|
||||
<< ", client_id: " << client_id_
|
||||
<< ", segment_id: " << usage.segment_id
|
||||
<< ", usage: " << usage.usage
|
||||
<< ", error: " << old_usage.error();
|
||||
sub_res.error = old_usage.error();
|
||||
result.sub_results.push_back(sub_res);
|
||||
continue;
|
||||
}
|
||||
|
||||
client_usage_ = client_usage_ - old_usage.value() + usage.usage;
|
||||
sub_res.error = ErrorCode::OK;
|
||||
result.sub_results.push_back(sub_res);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t P2PClientMeta::GetAvailableCapacity() const {
|
||||
SpinRWLockLocker lock(&capacity_mutex_, shared_lock);
|
||||
if (client_capacity_ <= client_usage_) return 0;
|
||||
return client_capacity_ - client_usage_;
|
||||
}
|
||||
|
||||
auto P2PClientMeta::CollectWriteRouteCandidates(
|
||||
const WriteRouteRequest& req, std::vector<WriteCandidate>& candidates)
|
||||
-> tl::expected<bool, ErrorCode> {
|
||||
SharedMutexLocker lock(&client_mutex_, shared_lock);
|
||||
|
||||
// Check health status under lock protection
|
||||
auto check_ret = InnerStatusCheck();
|
||||
if (!check_ret.has_value()) {
|
||||
LOG(WARNING) << "client could not route"
|
||||
<< ", client_id: " << client_id_;
|
||||
return false; // skip unhealthy client, candidaes are not enough
|
||||
}
|
||||
|
||||
// localhost is not allowed, skip current client
|
||||
if (!req.config.allow_local && client_id_ == req.client_id)
|
||||
return false; // candidaes are not enough
|
||||
|
||||
// iterate segments to find candidates
|
||||
bool trigger_stop_early = false;
|
||||
segment_manager_->ForEachSegment([&](const Segment& seg) -> bool {
|
||||
// In ForEachSegment callback:
|
||||
// 1. return false means continue to process next segment
|
||||
// (skip current segment or finish processing)
|
||||
// 2. return true means early stop
|
||||
size_t usage = seg.GetP2PExtra().usage;
|
||||
if (seg.size - usage < req.size)
|
||||
return false; // usage does not enough, candidaes are not enough
|
||||
|
||||
const auto& p2p_extra = seg.GetP2PExtra();
|
||||
|
||||
// exclude segments that contain any tags in tag_filters
|
||||
bool hit_filter_tag = false;
|
||||
for (const auto& tag : req.config.tag_filters) {
|
||||
if (std::find(p2p_extra.tags.begin(), p2p_extra.tags.end(), tag) !=
|
||||
p2p_extra.tags.end()) {
|
||||
hit_filter_tag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hit_filter_tag) return false; // hit excluding tag, skip segment
|
||||
|
||||
if (p2p_extra.priority < req.config.priority_limit)
|
||||
return false; // priority does not enough, candidaes are not enough
|
||||
|
||||
int priority = p2p_extra.priority;
|
||||
if (req.config.prefer_local && client_id_ == req.client_id) {
|
||||
// hit localhost and prefer local, add infinite priority
|
||||
priority += INF_PRIORITY;
|
||||
}
|
||||
|
||||
WriteCandidate candidate;
|
||||
candidate.available_capacity = seg.size - usage;
|
||||
candidate.priority = priority;
|
||||
candidate.replica.client_id = client_id_;
|
||||
candidate.replica.segment_id = seg.id;
|
||||
candidate.replica.ip_address = ip_address_;
|
||||
candidate.replica.rpc_port = rpc_port_;
|
||||
candidate.replica.object_size = req.size;
|
||||
|
||||
candidates.push_back(std::move(candidate));
|
||||
if (req.config.early_return &&
|
||||
candidates.size() >= req.config.max_candidates &&
|
||||
req.config.max_candidates !=
|
||||
WriteRouteRequestConfig::RETURN_ALL_CANDIDATES) {
|
||||
// current candidates are enough, early stop
|
||||
trigger_stop_early = true;
|
||||
return true; // stop ForEachSegment
|
||||
}
|
||||
return false; // process next segment
|
||||
});
|
||||
|
||||
// early stop, if trigger_stop_early == true, stop ForEachClient
|
||||
return trigger_stop_early;
|
||||
}
|
||||
|
||||
} // namespace mooncake
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue