Compare commits

...

11 Commits

Author SHA1 Message Date
66c8d61323 osd: Stabilize unstable writes of a removed pool before stopping its PGs
stop_pg() forgets the PG's unstable writes and leaves them to the next
primary, which stabilizes or rolls them back during peering. But nobody
peers a removed pool again, so its unstable writes stay on the OSDs
forever. The old store can't trim its journal past them: the journal
fills up and writes to all other pools on the same OSDs fail with
-EAGAIN, restarts don't help.

The monitor pauses PGs of a removed pool and waits for them to go
offline before it forgets them. Make the primary of such a PG stabilize
its unstable writes first, while the PG is still active and locked: it
refuses new writes, waits for the ones in flight, runs a sync, and then
stops the PG whatever the result. Pauses of existing pools (PG count
changes) stop at once as before.
2026-11-02 17:12:47 +03:00
a7a6d5f38e Add tests to readme-ru 2026-11-01 22:03:16 +03:00
01b501c686 Release 3.2.3
Bugfix release, also including a new PG chaos test

- Data correctness
  - Fixed fsync replay logic in the client library
  - Fixed possible incorrect marking of replicated objects as HAS_INCOMPLETE during a
    partially successful write while moving the object to another OSD set
  - Fixed PG hanging when the queue simultaneously contained an active
    write operation and an initial flush of the same object
  - Fixed a case of incorrect computation of EC object states during PG peering
  - Fixed possible read errors during concurrent writes or deletions
  - Fixed possible failure to report PG state when the OSD didn't acquire the lease yet
  - Fixed theoretically incorrect marking of replicated objects as DEGRADED on state change
  - Fixed read errors from images with more than 255 snapshots
  - Parallel removal of multiple snapshots of the same image from two processes now fails
    with an error instead of corrupting the snapshot chain
- New and old stores
  - New store: fixed big writes hanging after a single redirect_intent write
  - New store: fixed several potential data loss errors in rare cases found by chaos tests:
    incorrect checksum handling for empty writes, incorrect object write after its rollback
    to version zero, reuse after restart of sequence numbers below the completed_lsn in the header
  - Old store: fixed a rare OSD hang when deleting an object
- Network & security
  - Fixed transport encryption breaking with OpenSSL versions containing the unfixed bug #27302
  - Added options for QoS in RDMA networks with PFC: rdma_traffic_class, rdma_sl, rdma_set_flow_label
  - Vault: the client now retries loading keys on error automatically
  - Vault: fixed read errors from clones of encrypted images
  - Check read access to the parent image when creating a clone
  - Retry DNS resolve errors instead of killing the client process
- VitastorFS
  - Deleting a directory with many previously deleted files is now faster because fewer
    redundant keys are scanned in the key-value database
  - Fixed an error when moving a directory over another empty directory
- vitastor-cli
  - `fix` and `describe` no longer hang when one of the OSDs is unavailable: such OSDs are
    skipped with a warning, and the exit code indicates an incomplete result
  - `describe --inode` shows all objects of an image, not just one
  - `osd-tree` crashed on etcd errors
  - `ls --tree` hid images with a filtered-out parent
  - `ls-pgs` printed an empty table instead of the real error and showed misplaced data as degraded
  - `dd`: copying a pipe without `count=` copied only 1 MB, `oflag=append` didn't work,
    `status=none` and `status=noxfer` were ignored, devices like /dev/mem couldn't be read
  - `create-pool`/`modify-pool` now refuse pg_size > 64 and renaming a pool to an occupied
    name; `level_placement` rules can be reset with an empty value
  - `modify --resize` silently accepted a broken size like "1.5G"
  - `rm-data` didn't validate offset boundaries
  - Fixed minor differences between help, docs and the actual behaviour
- Deployment
  - Partitions with the Vitastor partition type are now owned by the vitastor user even when
    they don't yet contain a valid superblock
  - Fixed the QEMU driver build on QEMU versions patched by RHEL 9.1
  - Fixed a small memory leak when destroying the QEMU driver
  - The client now has a mode that handles fatal errors instead of silently killing the whole
    client process via exit(1). New QEMU driver versions use this mode and should no longer
    take the QEMU process down when a disk fails to connect
2026-11-01 20:10:42 +03:00
59e91f595d Fix parallel removal of snapshots (actually make it fail) by adding more CAS checks 2026-11-01 17:28:59 +03:00
d75df73c3a Add blockstore chaos test docs 2026-11-01 14:19:38 +03:00
f539bba0eb Add missing free()s to qemu_driver 2026-10-31 17:12:10 +03:00
ddcd3aef87 Allow to capture errors in client instead of just calling exit(1) 2026-10-31 14:36:58 +03:00
977b084af8 Change cluster_client_t::msgr to unique_ptr 2026-10-31 14:29:33 +03:00
e0b683a634 Fix config/this->config ambiguity in client constructor 2026-10-31 14:27:33 +03:00
ac808fefe7 Retry resolve errors instead of exiting 2026-10-31 12:15:11 +03:00
c4defad6bf Clear all timers on etcd_state_client_http destruction 2026-10-31 12:15:11 +03:00
63 changed files with 1555 additions and 218 deletions

View File

@@ -851,6 +851,24 @@ jobs:
echo ""
done
test_old_rm_pool_unstable:
runs-on: ubuntu-latest
needs: build
container: ${{env.TEST_IMAGE}}:${{github.sha}}
steps:
- name: Run test
id: test
timeout-minutes: 3
run: OLD=1 /root/vitastor/tests/test_rm_pool_unstable.sh
- name: Print logs
if: always() && steps.test.outcome == 'failure'
run: |
for i in /root/vitastor/testdata/*.log /root/vitastor/testdata/*.txt; do
echo "-------- $i --------"
cat $i
echo ""
done
test_snapshot_chain:
runs-on: ubuntu-latest
needs: build
@@ -1031,6 +1049,24 @@ jobs:
echo ""
done
test_snapshot_rm_parallel:
runs-on: ubuntu-latest
needs: build
container: ${{env.TEST_IMAGE}}:${{github.sha}}
steps:
- name: Run test
id: test
timeout-minutes: 3
run: /root/vitastor/tests/test_snapshot_rm_parallel.sh
- name: Print logs
if: always() && steps.test.outcome == 'failure'
run: |
for i in /root/vitastor/testdata/*.log /root/vitastor/testdata/*.txt; do
echo "-------- $i --------"
cat $i
echo ""
done
test_kv_stress:
runs-on: ubuntu-latest
needs: build

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 2.8...3.30)
project(vitastor)
set(VITASTOR_VERSION "3.2.2")
set(VITASTOR_VERSION "3.2.3")
include(CTest)

View File

@@ -75,6 +75,9 @@ Vitastor поддерживает QEMU-драйвер, протоколы UBLK,
- [NFS](docs/usage/nfs.ru.md) кластерная файловая система и псевдо-ФС прокси
- [Безопасность](docs/usage/security.ru.md)
- [Администрирование](docs/usage/admin.ru.md)
- Тесты
- [Хаос тест хранилища](docs/tests/blockstore-chaos.ru.md)
- [Хаос тест PG](docs/tests/pg-chaos.ru.md)
- Производительность
- [Понимание сути производительности](docs/performance/understanding.ru.md)
- [Теоретический максимум](docs/performance/theoretical.ru.md)

View File

@@ -75,6 +75,9 @@ Read more details in the documentation. You can start from here: [Quick Start](d
- [NFS](docs/usage/nfs.en.md) clustered file system and pseudo-FS proxy
- [Security](docs/usage/security.en.md)
- [Administration](docs/usage/admin.en.md)
- Tests
- [Blockstore chaos test](docs/tests/blockstore-chaos.en.md)
- [PG chaos test](docs/tests/pg-chaos.en.md)
- Performance
- [Understanding storage performance](docs/performance/understanding.en.md)
- [Theoretical performance](docs/performance/theoretical.en.md)

View File

@@ -1,4 +1,4 @@
VITASTOR_VERSION ?= v3.2.2
VITASTOR_VERSION ?= v3.2.3
all: build push

View File

@@ -49,7 +49,7 @@ spec:
capabilities:
add: ["SYS_ADMIN"]
allowPrivilegeEscalation: true
image: vitalif/vitastor-csi:v3.2.2
image: vitalif/vitastor-csi:v3.2.3
args:
- "--node=$(NODE_ID)"
- "--endpoint=$(CSI_ENDPOINT)"

View File

@@ -87,7 +87,7 @@ spec:
- name: socket-dir
mountPath: /csi
- name: csi-attacher
image: k8s.gcr.io/sig-storage/csi-attacher:v3.2.2
image: k8s.gcr.io/sig-storage/csi-attacher:v3.2.3
args:
- "--v=5"
- "--csi-address=$(ADDRESS)"
@@ -121,7 +121,7 @@ spec:
privileged: true
capabilities:
add: ["SYS_ADMIN"]
image: vitalif/vitastor-csi:v3.2.2
image: vitalif/vitastor-csi:v3.2.3
args:
- "--node=$(NODE_ID)"
- "--endpoint=$(CSI_ENDPOINT)"

View File

@@ -5,7 +5,7 @@ package vitastor
const (
vitastorCSIDriverName = "csi.vitastor.io"
vitastorCSIDriverVersion = "3.2.2"
vitastorCSIDriverVersion = "3.2.3"
)
// Config struct fills the parameters of request or user input

2
debian/changelog vendored
View File

@@ -1,4 +1,4 @@
vitastor (3.2.2-1) unstable; urgency=medium
vitastor (3.2.3-1) unstable; urgency=medium
* Bugfixes

View File

@@ -1,4 +1,4 @@
VITASTOR_VERSION ?= v3.2.2
VITASTOR_VERSION ?= v3.2.3
all: build push

View File

@@ -4,7 +4,7 @@
#
# Desired Vitastor version
VITASTOR_VERSION=v3.2.2
VITASTOR_VERSION=v3.2.3
# Additional arguments for all containers
# For example, you may want to specify a custom logging driver here

View File

@@ -26,9 +26,9 @@ at Vitastor Kubernetes operator: https://github.com/Antilles7227/vitastor-operat
The instruction is very simple.
1. Download a Docker image of the desired version: \
`docker pull vitalif/vitastor:v3.2.2`
`docker pull vitalif/vitastor:v3.2.3`
2. Install scripts to the host system: \
`docker run --rm -it -v /etc:/host-etc -v /usr/bin:/host-bin vitalif/vitastor:v3.2.2 install.sh`
`docker run --rm -it -v /etc:/host-etc -v /usr/bin:/host-bin vitalif/vitastor:v3.2.3 install.sh`
3. Reload udev rules: \
`udevadm control --reload-rules`
4. Enable the vitastor-host service: \

View File

@@ -25,9 +25,9 @@ Vitastor можно установить в Docker/Podman. При этом etcd,
Инструкция по установке максимально простая.
1. Скачайте Docker-образ желаемой версии: \
`docker pull vitalif/vitastor:v3.2.2`
`docker pull vitalif/vitastor:v3.2.3`
2. Установите скрипты в хост-систему командой: \
`docker run --rm -it -v /etc:/host-etc -v /usr/bin:/host-bin vitalif/vitastor:v3.2.2 install.sh`
`docker run --rm -it -v /etc:/host-etc -v /usr/bin:/host-bin vitalif/vitastor:v3.2.3 install.sh`
3. Перезагрузите правила udev: \
`udevadm control --reload-rules`
4. Включите сервис vitastor-host: \

View File

@@ -0,0 +1,229 @@
[Documentation](../../README.md#documentation) → Tests → The blockstore chaos-test model
-----
[Читать на русском](blockstore-chaos.ru.md)
# The blockstore chaos-test model
A seed-driven crash test of the blockstore (`src/test/test_blockstore_chaos.cpp`):
the real blockstore - journal, metadata, flusher, compaction, checksums, recovery -
running on a mock disk that lives entirely in RAM. Nothing above it exists: no OSD,
no PG, no network, no etcd. The test talks to the blockstore directly and checks
only what the storage itself promises. It is the counterpart of the
[PG chaos test](pg-chaos.en.md), which assumes the storage is correct.
Everything BELOW the blockstore is simulated: io_uring completions come back out of
submission order after a random delay, a volatile write cache holds part of every
write, and a power outage makes the writes that were in flight land fully, torn at a
sector boundary, or not at all, while a random half of the cache disappears. The
model of what was written is kept alongside, and after every outage the recovered
state is checked against it.
The same workload and the same checks run against both implementations - v1
(meta_format 2) and v2 (heap, meta_format 3) - which are supposed to have identical
semantics. The seed also picks the configuration, so a failing run is replayed by
passing back the seed it printed; `-h` lists the options.
The data is four 128 K objects (inode 1, four stripes) on one 256 MB mock disk that
carries metadata at offset 0, the journal from 16 M and the data from 32 M; write
granularity is 4 K.
## Configurations (24)
- v1 / v2 blockstore (meta_format 2 / 3) ×
- immediate_commit none/all ×
- `csum_block` none / 4096 / 16384 - 16384 is deliberately larger than the 4 K write
granularity, so a write covers only part of a checksummed block ×
- instant/two-phase writes, like in replicated/EC pools (BS_OP_WRITE_STABLE / BS_OP_WRITE+BS_OP_STABLE)
2 × 2 × 3 × 2 = 24; the configuration is `seed % 24`, `--config` overrides it. The
two write styles can't be mixed inside one object: a stable version over an unstable
one is rejected with EINVAL, and a pool is always one or the other.
## The world model
Per object the model keeps `submitted` (the highest sequence ever enqueued), `acked`,
`synced` and `durable`, plus maps from the sequence number to the full 128 K content
(an empty vector means "deleted"), to the version the store actually sees, and to
the bitmap byte. Sequence numbers only ever grow and key the model, because version
numbers restart from 1 after a deletion - the store explicitly allows writing low
versions over a delete. `deleted_upto` stops an old sync snapshot from making a
wiped-out version stabilizable, and `can_rollback` records that this instance really
did stabilize the durable version (recovery may have merged it into one clean entry).
Durability is derived purely from completions - there are no extra flags:
- with `immediate_commit=all` a completed write is immediately synced; with a
volatile disk cache nothing is synced until a SYNC that covers it completes;
- a synced deletion and a synced `BS_OP_WRITE_STABLE` write are stable by themselves
and so are durable at once; a two-phase write still has to be stabilized, and that
happens asynchronously, from a later step.
Object contents are self-identifying: 32-byte records `{seq, version, object,
offset}` whose size divides the 4 K granularity, so a record never straddles a write
or a read, and any mismatch decodes into "which write put these bytes here".
External bitmaps are self-identifying too: every write fills its 4-byte bitmap (one
bit per 4 K granule) with one recognisable byte. The blockstore treats the bitmap as
opaque, but it travels with the entry through compaction, recovery and rollbacks
just like the data does.
Invariant: only one operation at a time may touch an object. While its write, read,
stabilize or rollback is in flight nothing else may go near it, so the base content
of a write and the expected answer of a read cannot change underneath them.
## The simulated reality
- **Event loop**: one step is the completion of the earliest in-flight disk request,
and the virtual clock jumps straight to it, so the configured latencies don't make
the test slow. A single seeded PRNG drives the latencies, the workload and the
power-outage dice alike.
- **Disk simulator**: per-request delays are drawn uniformly (10-200 us for reads
and writes, 100-2000 us for fsync) - that is what makes completions arrive out of
order; 4 K sector; writes up to 4 K are atomic and land whole or not at
all. In buffered mode (`immediate_commit=none`) writes go into a volatile cache
and only reach the media on fsync. On `power_loss()` every in-flight write
lands fully, torn at a sector boundary, or not at all, and every cached write
survives with probability 50% - the survivors are committed to the disk and the
cache is emptied, so nothing from it disappears at the next outage. Error injection
for read/write/fsync exists but is off by default: the blockstore treats most write
errors as fatal, by design.
- **Ringloop stub**: an unknown fd aborts the test at once; `reset()` drops queued
and pending completions as if the process had died, so no stale callback can fire
into a destroyed blockstore. A power outage empties the disk's in-flight queue by
definition, and a clean restart waits for it to go idle explicitly.
## Operations (one per step)
Each step picks an action by weight (`--p-*`; 0 disables it):
- **wait (500)** - submit nothing, just let the simulation run one event.
- **write (60)** - write a random 4 K-aligned range of a random free object: a new
version (numbering restarts after a deletion), self-describing payload, its own
bitmap byte.
- **delete (6)** - delete an existing object; deleting a missing one is a no-op the
store answers without creating an entry, so it isn't exercised. A deletion is
stable by itself.
- **read (40)** - read a random 4 K-aligned range - deliberately NOT aligned to the
checksum block, so reads cut across checksum blocks and across the boundaries of
the extents that were actually written - or the whole object with `full_read`%
(25) chance; the answer is verified against the model when it completes.
- **sync (30)** - flush; what it covers is the set of acknowledged versions as of
submit time, treated as a lower bound. Not submitted while a rollback is in
flight: a rollback rewinds the test model - `acked` and `synced` drop back to the
stable version, everything above it disappears from it - so the snapshot would
name versions that no longer exist. This is a limit of the test model, not of the
blockstore API.
- **stabilize (50)** - commit a random non-empty prefix of everything that has
become stabilizable, as a single op carrying several (object, version) entries -
the shape an OSD uses; single-entry ops happen just as naturally.
- **rollback (6)** - throw the unstable versions away instead of committing them.
Two-phase mode only (an instant write is stable at once), and only for objects
that have a stable non-deleted version below the newest one. On success everything
above the stable version is gone and the store's version counter for the object
goes back to it. Not submitted while a sync is in flight, for the same reason,
nor while another rollback is in flight.
- **restart (1)** - a CLEAN restart: drain everything in flight, read all objects,
wait for the disk to go idle, destroy the blockstore, create it again, read
everything again.
- **crash (2)** - pull the plug right now: power loss, whatever was in flight is
accounted as a legal-but-unconfirmed outcome, the blockstore is destroyed and
recreated, every object is checked against the recovery rule.
At most `max_inflight` (8) operations are in flight at once; a step that finds the
queue full waits for one completion instead of submitting anything - that is where
hangs are checked.
At the end of the run everything still in flight is drained, and every object is
read whole and verified.
## Op outcome accounting
- Completed with the expected retval - the content is recorded at its sequence
number, `acked` moves to it, and the follow-up (synced / durable / needs-stabilize)
is derived from the configuration.
- A write or delete with `-EAGAIN` - a refusal, not a loss (the journal is full and
the flusher can't keep up under pressure); the model is unchanged.
- A failed sync - it simply didn't sync anything; also legal under pressure.
- A read interrupted by an outage tells nothing and is dropped.
- Anything else - including a failed stabilize or rollback - aborts the test with
the affected (object, version) entries printed.
- A write that was in flight at the moment of the outage: its content is recorded as
a legal outcome all the same, but `acked` doesn't move, so it may still be lost.
## Checks (every abort point)
**Data consistency:**
1. a read of a deleted or never-written object returns `-ENOENT`;
2. a read returns exactly the acknowledged content: `retval == len` and byte-for-byte
equality over the whole range; the first differing record is printed with both
obj/seq/version/offset values, so stale data names its own author;
3. the returned external bitmap must belong to the returned version ("returned the
bitmap of another version");
4. after a crash or a restart a read answers either the whole object or `-ENOENT` -
any other retval means torn data was committed;
5. whatever came back must be the content of a version at or above the guaranteed
floor: the last durable version after a power outage, the last acknowledged one
(the last synced one with a volatile cache) after a restart that lost nothing.
Anything else aborts the test with "came back ... holding unexpected content"
plus a diff against every candidate version - the one that almost matches usually
tells which write was lost or applied out of order;
6. the object may legitimately be gone only when nothing was guaranteed at all; then
the model resets and numbering starts over from 1;
7. with `immediate_commit=all` a clean restart must return exactly the same state -
same retval, same version, same bytes; the failure also prints the first
never-written journal sector, which points straight at the lost record;
8. what came back becomes the new baseline but not the new durable point: the
durable content stays a legal outcome, `can_rollback` is cleared, and the next
outage may still roll the object back to the durable version;
9. a rollback may only remove versions above the stable one, and the version counter
is rewound accordingly - later writes and reads would notice otherwise;
10. the final whole-object reads must match the acknowledged content.
**Liveness:**
11. "operations did not complete in 1000000 steps - deadlock?" - the stuck
operations are printed together with the store's diagnostics
(completed/fsynced/compacted LSNs, compact queue) in the v2 implementation;
12. "blockstore failed to start";
13. "a read of object N did not complete in MAX_STEPS - deadlock?" and "the final
read of object N did not complete";
14. "disk did not go idle before a clean restart" - a use-after-free guard: no
request may outlive the blockstore that submitted it.
**Failure accounting:**
15. only `-EAGAIN` is accepted as a refusal for a write/delete, a failed sync is
treated as "synced nothing", and any other error, including a failed stabilize
or rollback, aborts the test.
**Test-stub guards:**
16. an unexpected fd on the ringloop stub, a rejected SQE, `--max-inflight < 1`;
17. overlapping concurrent writes: a write submitted while another in-flight write
still covers part of its range aborts the test - a device may apply such a pair
in either order, so relying on the newer one winning is a bug, and catching it
at submission time is deterministic instead of waiting for an unlucky completion
order.
## Reporting and reproducibility
By default the test runs 48 fixed seeds (every configuration twice) plus 152 random
ones spread evenly over the configurations - 200 seeds × 10000 steps in total. Every
seed runs in its own child process, so one abort doesn't take the whole sweep down,
and the failing seeds are listed at the end. `--seed N` runs a single seed
in-process, which is what you want under a debugger; `--seeds a-b`, `--random n`,
`--steps n`, `--config n` and the `--p-*` weights shape the run (e.g.
`--p-delete 0 --p-rollback 0` gives a workload without deletions and rollbacks).
The OK line: writes (refused with EAGAIN) / deletes / syncs / stabilizes / rollbacks
/ crashes / clean restarts / verified reads / disk ops.
Determinism: one seed -> the same configuration and the same schedule; `-v` traces
every operation and every simulated disk request, including its simulated latency,
the power-loss decision taken for every in-flight write and every cached write
dropped from the volatile cache.
Built by `make build_tests` and run by ctest as `test_blockstore_chaos`.

View File

@@ -0,0 +1,211 @@
[Документация](../../README-ru.md#документация) → Тесты → Модель хаос-теста blockstore (`src/test/test_blockstore_chaos.cpp`)
-----
[Read in English](blockstore-chaos.en.md)
# Модель хаос-теста blockstore
Seed-driven тест на отказа питания (`src/test/test_blockstore_chaos.cpp`): настоящее хранилище —
журнал, метаданные, flusher, compaction, контрольные суммы, recovery — работает на заглушке диска,
полностью в RAM. Ничего над ним нет: ни OSD, ни PG, ни сети, ни etcd. Тест обращается к хранилищу
напрямую и проверяет только то, что обещает само хранилище. Это пара к
[хаос-тесту PG](pg-chaos.ru.md), который считает хранилище корректным.
Всё, что НИЖЕ хранилища, симулируется: ответы io_uring приходят не в порядке отправки и со
случайной задержкой, волатильный кэш записи удерживает часть записей, а потеря питания делает так,
что бывшие в полёте записи сохраняются целиком, рвутся по границе сектора или не сохраняются вовсе,
случайная половина кэша исчезает. Рядом ведётся модель того, что было записано, и после
каждой имитации отказа восстановленное состояние сверяется с ней.
Один и тот же набор нагрузок и проверок прогоняется на обеих реализациях — v1 (meta_format 2)
и v2 (heap, meta_format 3), которые обязаны вести себя одинаково. Сид также задаёт
конфигурацию, поэтому упавший прогон воспроизводится тем же сидом; опции — по `-h`.
Данные: четыре объекта по 128 К (inode 1, четыре stripe) на одном mock-диске в 256 МБ, где
метаданные лежат с нуля, журнал — с 16 М, данные — с 32 М; гранула записи 4 К.
## Конфигурации (24)
- blockstore v1 / v2 (meta_format 2 / 3) ×
- immediate_commit none/all ×
- csum_block нет / 4096 / 16384 — 16384 намеренно больше 4 К гранулы записи, то есть запись
покрывает лишь часть чексуммного блока ×
- мгновенная/двухфазная запись, как в репликах/EC (BS_OP_WRITE_STABLE / BS_OP_WRITE+BS_OP_STABLE)
2 × 2 × 3 × 2 = 24; конфигурация = `seed % 24`, переопределяется через `--config`. Смешать два
стиля записей в одном объекте нельзя: stable-версия поверх unstable отклоняется с EINVAL, а пул —
всегда что-то одно.
## Модель мира
Для каждого объекта хранятся `submitted` (максимальная отправленная последовательность), `acked`,
`synced`, `durable`, а также отображения номер-последовательности → полное содержимое 128 К
(пустой вектор = «удалён»), → номер версии, который видит хранилище, и → байт битмапа.
Номер последовательности только растёт и именно он является ключом модели, потому что после
удаления версии снова начинаются с 1 — хранилище явно разрешает писать низкие версии поверх
удаления. `deleted_upto` не даёт старому снимку sync сделать stabilizable стёртую версию, а
`can_rollback` помечает, что именно этот экземпляр действительно стабилизировал durable-версию
(recovery мог слить её в одну чистую запись).
Зафиксированность выводится только из завершения операций, дополнительных флагов нет:
- при `immediate_commit=all` завершённая запись сразу считается synced; при волатильном кэше
диска — ничто не synced, пока не завершится накрывающий её SYNC;
- synced-удаление и synced-запись `BS_OP_WRITE_STABLE` стабильны сами по себе и сразу становятся
durable; двухфазную запись надо ещё стабилизировать, и это происходит асинхронно, позже, из
другого шага.
Содержимое объектов самоидентифицируется: 32-байтные записи `{seq, версия, объект, смещение}`,
размер которых делит гранулу 4 К, поэтому такая запись никогда не пересекает границу операции
записи или чтения, и любое расхождение декодируется в «какая версия записала эти байты».
Внешние битмапы тоже самоидентифицируются: каждая запись заполняет свой 4-байтный битмап
(один бит на 4 К гранулу) одним узнаваемым байтом. Для хранилища битмап непрозрачен, но он
путешествует вместе с записью через compaction, recovery и откаты так же, как данные.
Инвариант: за раз с объектом работает только одна операция. Пока в полёте его запись, чтение,
stabilize или rollback, ничто другое к нему не подходит — поэтому базовое содержимое записи и
ожидаемый ответ чтения не могут измениться под ними.
## Симулируемая реальность
- **Цикл событий**: один шаг — это завершение самого раннего запроса диска, и виртуальные часы
прыгают прямо к нему, поэтому заданные задержки не замедляют тест. Один seed-driven PRNG
задаёт и задержки, и нагрузку, и броски «костей» при отказе питания.
- **Симулятор диска**: задержка на каждый запрос берётся равномерно (10–200 мкс на чтение и
запись, 100–2000 мкс на fsync) — именно из-за разброса completion'ы приходят не по порядку;
сектор 4 К; записи до 4 К атомарны и ложатся на диск целиком или не ложатся вовсе. В буферном
режиме (`immediate_commit=none`) записи попадают в волатильный кэш и доходят до носителя только
по fsync. При `power_loss()` каждая запись в полёте сохраняется целиком, рвётся по границе сектора или
не сохранятся вовсе, а записи в кэше выживают и сохраняются на диск с вероятностью 50%, после
чего кэш очищается, т.к. при следующем запуске он должен быть пустым и не исчезать при ещё одном
отказе. Инъекция ошибок read/write/fsync существует, но по умолчанию выключена: хранилище по замыслу
считает большинство ошибок записи фатальными.
- **Заглушка ringloop**: неизвестный fd немедленно обрывает тест; `reset()` выбрасывает очереди и
ожидающие completion'ы, как если бы процесс умер, — ни один «просроченный» колбэк не может
прилететь в уничтоженное хранилище. Отказ питания по определению очищает очередь in-flight диска,
а чистая перезагрузка дожидается простоя диска явно.
## Операции (одна на шаг)
На каждом шаге действие выбирается по весу (`--p-*`; 0 выключает его):
- **wait (500)** — ничего не слать, прокрутить одно событие симуляции.
- **write (60)** — запись случайного выровненного по 4 К диапазона случайного свободного объекта:
новая версия (нумерация после удаления начинается заново), самоидентифицируемое содержимое, свой
битмап.
- **delete (6)** — удаление существующего объекта; удаление несуществующего — no-op, на который
хранилище отвечает без создания записи, поэтому он не тестируется. Удаление стабильно само по себе.
- **read (40)** — чтение случайного диапазона: выровненного по 4 К, но — назло — не выровненного по
размеру чексуммного блока, чтобы чтения резали и чексуммные блоки, и границы реально записанных
экстентов; либо целиком с вероятностью `full_read`% (25). Ответ проверяется по модели при
завершении.
- **sync (30)** — сброс кэша; покрытие — множество подтверждённых версий на момент отправки, оно
трактуется как нижняя граница. Не отправляется, пока в полёте rollback: rollback откатывает
модель теста — `acked` и `synced` возвращаются к стабильной версии, и всё выше неё из модели
исчезает, — поэтому снимок покрытия начал бы ссылаться на версии, которых уже нет. Это
ограничение модели теста, а не API хранилища.
- **stabilize (50)** — зафиксировать случайный непустой префикс всего ставшего stabilizable одной
операцией с несколькими парами (объект, версия) — ровно так, как делает OSD; одноэлементные
операции при этом возникают так же естественно.
- **rollback (6)** — выбросить нестабильные версии вместо фиксации. Только двухфазный режим
(мгновенная запись стабильна сразу) и только для объектов со стабильной неудалённой версией ниже
последней. После успеха всё выше стабильной версии исчезает, и счётчик версий объекта в
хранилище откатывается к ней. Не отправляется, пока в полёте sync (по той же причине) и пока
в полёте другой rollback.
- **restart (1)** — ЧИСТАЯ перезагрузка: дождаться завершения всего в полёте, прочитать все
объекты, дождаться простоя диска, уничтожить хранилище, создать заново, прочитать снова.
- **crash (2)** — выдернуть шнур прямо сейчас: потеря питания, всё бывшее в полёте учитывается как
легальный, но неподтверждённый исход, хранилище уничтожается и создаётся заново, каждый объект
проверяется по правилу восстановления.
Одновременно выполняется не более `max_inflight` (8) операций; шаг при полной очереди вместо
отправки ждёт завершения одной из них — здесь проверяется зависание.
В конце работы всё оставшееся в полёте дожидается, после чего каждый объект читается целиком и
проверяется.
## Учёт результатов операций (account)
- Завершилась с ожидаемым кодом возврата — содержимое записано в модель под своей
последовательностью, `acked` сдвинут, а дальнейшее (synced / durable / требует stabilize)
выводится из конфигурации.
- Запись или удаление с `-EAGAIN` — отказ, а не потеря (журнал заполнен, flusher не успевает под
нагрузкой); модель не меняется.
- Упавший sync — значит, он ничего и не синхронизировал; это тоже легально под нагрузкой.
- Чтение, прерванное отказом питания, ничего не сообщает и просто отбрасывается.
- Всё остальное, включая упавшие stabilize/rollback, — падение теста с печатью затронутых пар
(объект, версия).
- Запись, бывшая в полёте в момент отказа: её содержимое так же записывается в модель как легальный
исход, но `acked` не сдвигается, то есть она всё ещё может быть потеряна.
## Проверки (все точки отмены)
**Согласованность данных:**
1. чтение удалённого или никогда не писанного объекта обязано вернуть `-ENOENT`;
2. чтение обязано вернуть ровно подтверждённое содержимое: `retval == len` и побайтовое совпадение
по всему диапазону; первая расходящаяся 32-байтная запись печатается с obj/seq/version/offset и
ожидаемым значением — то есть устаревшие данные сами называют своего автора;
3. внешний битмап в ответе обязан принадлежать именно возвращённой версии
(«returned the bitmap of another version»);
4. после отказа питания или перезапуска чтение отвечает либо целым объектом, либо `-ENOENT` —
любой иной retval означает, что на диск попало порванное содержимое;
5. то, что объект «вернулся», обязано быть содержимым версии не ниже гарантированной границы:
последней durable — после отказа питания, последней подтверждённой (последней synced при
волатильном кэше) — после перезапуска, который ничего не потерял. Иначе — падение с
«came back ... holding unexpected content» и диффом по каждой версии-кандидату: та, что почти
совпадает, обычно и показывает, какая запись потерялась или применилась не в том порядке;
6. объект вправе полностью исчезнуть только тогда, когда ничего не гарантировалось; тогда модель
сбрасывается, и нумерация начинается с 1;
7. при `immediate_commit=all` чистая перезагрузка обязана вернуть ровно то же состояние — тот же
retval, та же версия, те же байты; в сообщении о падении печатается ещё и первый никогда не
писанный сектор журнала, который прямо указывает на потерянную запись;
8. вернувшееся содержимое становится новой базой, но не новой durable-точкой: durable-содержимое
остаётся легальным исходом, `can_rollback` сбрасывается, и следующий отказ всё ещё может
откатить объект к durable-версии;
9. rollback вправе удалить только версии выше стабильной, и счётчик версий откатывается
соответственно — иначе это заметят последующие записи и чтения;
10. финальные чтения целых объектов обязаны совпасть с подтверждённым содержимым.
**Живость:**
11. «operations did not complete in 1000000 steps — deadlock?» — зависшие операции печатаются
вместе с диагностикой хранилища (completed/fsynced/compacted LSN, очередь compact) в v2;
12. «blockstore failed to start»;
13. «a read of object N did not complete in MAX_STEPS — deadlock?» и «the final read of object N
did not complete»;
14. «disk did not go idle before a clean restart» — защита от use-after-free: ни один запрос не
может пережить хранилище, которое его послало.
**Учёт отказов:**
15. отказом записи/удаления принимается только `-EAGAIN`, упавший sync трактуется как «ничего не
синхронизировал», а любая другая ошибка — включая stabilize/rollback — обрывает тест.
**Защиты тестовых заглушек:**
16. неожиданный fd в заглушке ringloop, отклонённый SQE, `--max-inflight < 1`;
17. перекрывающиеся одновременные записи: запись, отправленная, пока в полёте находится другая,
накрывающая часть её диапазона, — падение теста. Устройство вправе применить такую пару в
любом порядке, поэтому полагаться на то, что победит более новая, — ошибка; ловить это на
отправке детерминированно лучше, чем ждать неудачного порядка завершения.
## Отчёт и воспроизводимость
По умолчанию тест прогоняет 48 фиксированных сидов (каждая конфигурация дважды) плюс 152 случайных,
равномерно разложенных по конфигурациям, — всего 200 сидов × 10000 шагов. Каждое сид выполняется в
отдельном дочернем процессе, поэтому одно падение не роняет весь прогон, а упавшие сиды печатаются
в конце. `--seed N` выполняет одно сид в том же процессе — именно это нужно под отладчиком;
`--seeds a-b`, `--random n`, `--steps n`, `--config n` и веса `--p-*` настраивают прогон
(например, `--p-delete 0 --p-rollback 0` даёт нагрузку без удалений и откатов).
OK-строка: writes (refused with EAGAIN) / deletes / syncs / stabilizes / rollbacks / crashes /
clean restarts / verified reads / disk ops.
Детерминизм: один сид ⇒ та же конфигурация и то же расписание; `-v` даёт трассу каждой операции и
каждого запроса к диску, включая смоделированную задержку, решение при отказе питания по каждой
бывшей в полёте записи и каждую запись, выброшенную из волатильного кэша.
Собирается через `make build_tests`, запускается ctest'ом как `test_blockstore_chaos`.

View File

@@ -18,7 +18,7 @@ the same `--seed`.
What it mainly verifies is the PG machinery (the subop fan-out,
peering/LIST/rollback/stabilize, repeer daemons, stop_pg, INCOMPLETE, recovery) and
partly the primary OSD's storage; checking the storage is not the test's main goal -
test_blockstore_chaos covers it.
it is covered by the [blockstore chaos test](blockstore-chaos.en.md).
## Configurations (10)

View File

@@ -15,7 +15,7 @@ EC 2+2 (members 2..4, spare 5, объект = один parity-stripe). Сид о
Проверяется в первую очередь PG-машинерия (subop-веер, peering/LIST/rollback/stabilize, repeer-демоны,
stop_pg, INCOMPLETE, recovery) и частично хранилище первичного OSD; однако, проверка хранилища —
не главная цель теста, его покрывает test_blockstore_chaos.
не главная цель теста, его покрывает [хаос-тест blockstore](blockstore-chaos.ru.md).
## Конфигурации (10)

View File

@@ -1,6 +1,6 @@
{
"name": "vitastor-mon",
"version": "3.2.2",
"version": "3.2.3",
"description": "Vitastor SDS monitor service",
"main": "mon-main.js",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "vitastor",
"version": "3.2.2",
"version": "3.2.3",
"description": "Low-level native bindings to Vitastor client library",
"main": "index.js",
"keywords": [

View File

@@ -50,7 +50,7 @@ from cinder.volume import configuration
from cinder.volume import driver
from cinder.volume import volume_utils
VITASTOR_VERSION = '3.2.2'
VITASTOR_VERSION = '3.2.3'
LOG = logging.getLogger(__name__)

View File

@@ -1,11 +1,11 @@
Name: vitastor
Version: 3.2.2
Version: 3.2.3
Release: 1%{?dist}
Summary: Vitastor, a fast software-defined clustered block storage
License: Vitastor Network Public License 1.1
URL: https://vitastor.io/
Source0: vitastor-3.2.2.el10.tar.gz
Source0: vitastor-3.2.3.el10.tar.gz
BuildRequires: gperftools-devel
BuildRequires: gcc-c++

View File

@@ -1,11 +1,11 @@
Name: vitastor
Version: 3.2.2
Version: 3.2.3
Release: 1%{?dist}
Summary: Vitastor, a fast software-defined clustered block storage
License: Vitastor Network Public License 1.1
URL: https://vitastor.io/
Source0: vitastor-3.2.2.el7.tar.gz
Source0: vitastor-3.2.3.el7.tar.gz
BuildRequires: gperftools-devel
BuildRequires: devtoolset-9-gcc-c++

View File

@@ -1,11 +1,11 @@
Name: vitastor
Version: 3.2.2
Version: 3.2.3
Release: 1%{?dist}
Summary: Vitastor, a fast software-defined clustered block storage
License: Vitastor Network Public License 1.1
URL: https://vitastor.io/
Source0: vitastor-3.2.2.el8.tar.gz
Source0: vitastor-3.2.3.el8.tar.gz
BuildRequires: gperftools-devel
BuildRequires: gcc-toolset-9-gcc-c++

View File

@@ -1,11 +1,11 @@
Name: vitastor
Version: 3.2.2
Version: 3.2.3
Release: 1%{?dist}
Summary: Vitastor, a fast software-defined clustered block storage
License: Vitastor Network Public License 1.1
URL: https://vitastor.io/
Source0: vitastor-3.2.2.el9.tar.gz
Source0: vitastor-3.2.3.el9.tar.gz
BuildRequires: gperftools-devel
BuildRequires: gcc-c++

View File

@@ -24,7 +24,7 @@ set(ENABLE_COVERAGE false CACHE BOOL "Enable code coverage")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_definitions(-DVITASTOR_VERSION="3.2.2")
add_definitions(-DVITASTOR_VERSION="3.2.3")
add_definitions(-D_GNU_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Wall -Wno-c99-designator -Wno-vla-cxx-extension -Wno-sign-compare -Wno-comment -Wno-parentheses -Wno-pointer-arith -fdiagnostics-color=always -fno-omit-frame-pointer -fvisibility=hidden -I ${CMAKE_SOURCE_DIR}/src)
add_link_options(-fno-omit-frame-pointer)
if (${WITH_ASAN})

View File

@@ -12,23 +12,27 @@
#define TRY_SEND_CONNECTING 1
#define TRY_SEND_OK 2
cluster_client_t::cluster_client_t(ring_loop_t *ringloop, timerfd_manager_t *tfd, json11::Json config, std::unique_ptr<etcd_state_client_t> st_cli_ptr)
cluster_client_t::cluster_client_t(ring_loop_t *ringloop, timerfd_manager_t *tfd, json11::Json passed_config, std::unique_ptr<etcd_state_client_t> st_cli_ptr)
{
wb = new writeback_cache_t();
wb->parent = this;
cli_config = config.object_items();
file_config = osd_messenger_t::read_config(config);
cli_config = passed_config.object_items();
file_config = osd_messenger_t::read_config(passed_config);
config = osd_messenger_t::merge_configs(cli_config, file_config, etcd_global_config, {});
this->exit_on_error = config.find("client_exit_on_error") == config.end()
? true : config["client_exit_on_error"].bool_value();
this->ringloop = ringloop;
this->tfd = tfd;
msgr.osd_num = 0;
msgr.tfd = tfd;
msgr.ringloop = ringloop;
msgr.repeer_pgs = [this](osd_num_t peer_osd)
msgr = new osd_messenger_t();
msgr->osd_num = 0;
msgr->tfd = tfd;
msgr->ringloop = ringloop;
msgr->repeer_pgs = [this](osd_num_t peer_osd)
{
if (msgr.osd_peers.find(peer_osd) != msgr.osd_peers.end())
if (msgr->osd_peers.find(peer_osd) != msgr->osd_peers.end())
{
// peer_osd just connected
continue_ops();
@@ -45,14 +49,15 @@ cluster_client_t::cluster_client_t(ring_loop_t *ringloop, timerfd_manager_t *tfd
}
}
};
msgr.exec_op = [this](osd_op_t *op)
msgr->exec_op = [this](osd_op_t *op)
{
// Garbage in
fprintf(stderr, "Can't handle incoming operation from client %ju\n", op->client_id);
msgr.stop_client(op->client_id);
msgr->stop_client(op->client_id);
delete op;
};
msgr.parse_config(config, true);
msgr->on_fatal_error_hook = [this](int errcode, const std::string & error) { fatal_error(errcode, error); };
msgr->parse_config(config, true);
st_cli = std::move(st_cli_ptr);
st_cli->on_load_config_hook = [this](json11::Json::object & cfg) { on_load_config_hook(cfg); };
@@ -64,6 +69,7 @@ cluster_client_t::cluster_client_t(ring_loop_t *ringloop, timerfd_manager_t *tfd
st_cli->on_load_pgs_hook = [this](bool success) { on_load_pgs_hook(success); };
st_cli->on_reload_hook = [this]() { this->st_cli->load_global_config(); };
st_cli->on_inode_change_hook = [this](uint64_t inode, bool removed) { on_change_inode_hook(inode, removed); };
st_cli->on_fatal_error_hook = msgr->on_fatal_error_hook;
st_cli->parse_config(config);
st_cli->infinite_start = false;
@@ -72,11 +78,21 @@ cluster_client_t::cluster_client_t(ring_loop_t *ringloop, timerfd_manager_t *tfd
st_cli->infinite_start = config["client_infinite_start"].bool_value();
}
st_cli->load_global_config();
if (ringloop)
{
ringloop->loop();
}
}
void cluster_client_t::set_on_fatal_error_hook(std::function<void(int, const std::string&)> cb)
{
on_fatal_error_hook = cb;
}
cluster_client_t::~cluster_client_t()
{
vault_destroy();
aborted = ECANCELED;
if (retry_timeout_id >= 0)
{
tfd->clear_timer(retry_timeout_id);
@@ -89,13 +105,44 @@ cluster_client_t::~cluster_client_t()
list_retry_timeout_id = -1;
list_retry_time = {};
}
msgr.repeer_pgs = [](osd_num_t){};
msgr->repeer_pgs = [](osd_num_t){};
if (ringloop)
{
ringloop->unregister_consumer(&consumer);
}
delete msgr;
msgr = NULL;
while (op_queue_head)
{
op_queue_head->retval = -ECANCELED;
erase_op(op_queue_head);
}
delete wb;
wb = NULL;
vault_destroy();
}
void cluster_client_t::fatal_error(int errcode, const std::string & error)
{
fprintf(stderr, "Error: %s%s\n", error.c_str(), exit_on_error ? ", exiting" : "");
if (exit_on_error)
{
// Simple old behaviour by default - stop the whole process
exit(1);
}
if (!pgs_loaded)
{
// Client becomes "ready" (but aborted) on error
for (auto fn: on_ready_hooks)
fn();
on_ready_hooks.clear();
}
if (!aborted && on_fatal_error_hook)
{
(std::function<void(int, const std::string&)>(on_fatal_error_hook))(errcode, error);
}
aborted = true;
abort_error = error;
}
cluster_op_t::~cluster_op_t()
@@ -150,13 +197,15 @@ std::vector<osd_num_t> cluster_op_t::get_left_on_dead()
void cluster_client_t::continue_raw_ops(osd_num_t peer_osd)
{
if (aborted)
return;
auto it = raw_ops.find(peer_osd);
while (it != raw_ops.end() && it->first == peer_osd)
{
auto op = it->second;
op->op_type = OSD_OP_OUT;
op->client_id = msgr.osd_peers.at(peer_osd)->client_id;
msgr.outbox_push(op);
op->client_id = msgr->osd_peers.at(peer_osd)->client_id;
msgr->outbox_push(op);
raw_ops.erase(it++);
}
}
@@ -165,14 +214,14 @@ void cluster_client_t::init_msgr()
{
if (msgr_initialized)
return;
msgr.init();
msgr->init();
msgr_initialized = true;
if (ringloop)
{
consumer.loop = [this]()
{
msgr.read_requests();
msgr.send_replies();
msgr->read_requests();
msgr->send_replies();
this->ringloop->submit();
};
ringloop->register_consumer(&consumer);
@@ -482,7 +531,7 @@ void cluster_client_t::on_load_config_hook(json11::Json::object & etcd_global_co
}
// vault
vault_parse_config();
msgr.parse_config(config, false);
msgr->parse_config(config, false);
st_cli->parse_config(config);
st_cli->load_pgs();
}
@@ -636,9 +685,9 @@ bool cluster_client_t::get_immediate_commit(uint64_t inode)
void cluster_client_t::on_change_osd_state_hook(osd_num_t peer_osd)
{
osd_tree_metrics.erase(peer_osd);
if (msgr.wanted_peers.find(peer_osd) != msgr.wanted_peers.end())
if (msgr->wanted_peers.find(peer_osd) != msgr->wanted_peers.end())
{
msgr.connect_peer(peer_osd, st_cli->peer_states[peer_osd]);
msgr->connect_peer(peer_osd, st_cli->peer_states[peer_osd]);
continue_lists();
}
}
@@ -651,12 +700,21 @@ void cluster_client_t::on_change_node_placement_hook()
bool cluster_client_t::is_ready()
{
return pgs_loaded;
return pgs_loaded || aborted;
}
int cluster_client_t::is_failed(std::string & error)
{
if (aborted)
{
error = abort_error;
}
return aborted;
}
void cluster_client_t::on_ready(std::function<void(void)> fn)
{
if (pgs_loaded)
if (is_ready())
{
fn();
}
@@ -855,6 +913,13 @@ void cluster_client_t::execute_internal(cluster_op_t *op)
void cluster_client_t::execute_cas(cluster_op_t *op, bool nosync)
{
if (aborted)
{
op->retval = -ECANCELED;
auto cb = std::move(op->callback);
cb(op);
return;
}
if ((op->opcode == OSD_OP_WRITE || op->opcode == OSD_OP_DELETE) && op->version && op->parts.size() > 1)
{
// Atomic writes to multiple stripes are unsupported
@@ -871,7 +936,14 @@ void cluster_client_t::execute_cas(cluster_op_t *op, bool nosync)
sync_op->flags = 0;
sync_op->callback = [this, op](cluster_op_t* sync_op)
{
execute_cas(op, true);
if (sync_op->retval != 0)
{
op->retval = sync_op->retval;
auto cb = std::move(op->callback);
cb(op);
}
else
execute_cas(op, true);
delete sync_op;
};
execute_internal(sync_op);
@@ -887,18 +959,25 @@ void cluster_client_t::execute_cas(cluster_op_t *op, bool nosync)
op->needs_reslice = false;
int res = try_send(op, 0, [this, op](osd_op_t *part)
{
if (aborted)
{
op->retval = -ECANCELED;
auto cb = std::move(op->callback);
cb(op);
return;
}
int expected = part->req.hdr.opcode == OSD_OP_DELETE ? 0 : part->req.rw.len;
op->retval = part->reply.hdr.retval;
if (op->retval != expected && op->retval >= 0)
op->retval = -EIO;
op->retval = op->retval == -EPIPE ? -EINTR : op->retval;
auto peer_it = msgr.osd_peers.find(op->parts[0].osd_num);
auto peer_it = msgr->osd_peers.find(op->parts[0].osd_num);
if (op->retval != expected || (op->flags & OP_IMMEDIATE_COMMIT))
{
auto cb = std::move(op->callback);
cb(op);
}
else if (peer_it == msgr.osd_peers.end())
else if (peer_it == msgr->osd_peers.end())
{
// Care must be taken to make sure that the client doesn't reconnect to the OSD
// before executing the previously completed operation callback (!)
@@ -930,7 +1009,7 @@ void cluster_client_t::execute_cas(cluster_op_t *op, bool nosync)
cb(op);
},
};
msgr.outbox_push(part);
msgr->outbox_push(part);
}
});
if (res == TRY_SEND_CONNECTING || res == TRY_SEND_OFFLINE)
@@ -1061,23 +1140,25 @@ bool cluster_client_t::check_rw(cluster_op_t *op)
void cluster_client_t::execute_raw(osd_num_t osd_num, osd_op_t *op)
{
auto peer_it = msgr.osd_peers.find(osd_num);
if (peer_it != msgr.osd_peers.end())
auto peer_it = msgr->osd_peers.find(osd_num);
if (peer_it != msgr->osd_peers.end())
{
op->op_type = OSD_OP_OUT;
op->client_id = peer_it->second->client_id;
msgr.outbox_push(op);
msgr->outbox_push(op);
}
else
{
if (msgr.wanted_peers.find(osd_num) == msgr.wanted_peers.end())
msgr.connect_peer(osd_num, st_cli->peer_states[osd_num]);
if (msgr->wanted_peers.find(osd_num) == msgr->wanted_peers.end())
msgr->connect_peer(osd_num, st_cli->peer_states[osd_num]);
raw_ops.emplace(osd_num, op);
}
}
int cluster_client_t::continue_rw(cluster_op_t *op)
{
if (aborted)
return 0;
if (op->state == 0)
goto resume_0;
else if (op->state == 1)
@@ -1450,8 +1531,8 @@ int cluster_client_t::try_send(cluster_op_t *op, int i, std::function<void(osd_o
primary_osd = nearest_osd;
}
part->osd_num = primary_osd;
auto peer_it = msgr.osd_peers.find(primary_osd);
if (peer_it != msgr.osd_peers.end())
auto peer_it = msgr->osd_peers.find(primary_osd);
if (peer_it != msgr->osd_peers.end())
{
osd_client_t *cl = peer_it->second;
part->flags |= PART_SENT|PART_VALID;
@@ -1495,12 +1576,12 @@ int cluster_client_t::try_send(cluster_op_t *op, int i, std::function<void(osd_o
},
};
part->op.iov = part->iov;
msgr.outbox_push(&part->op);
msgr->outbox_push(&part->op);
return TRY_SEND_OK;
}
else if (msgr.wanted_peers.find(primary_osd) == msgr.wanted_peers.end())
else if (msgr->wanted_peers.find(primary_osd) == msgr->wanted_peers.end())
{
msgr.connect_peer(primary_osd, st_cli->peer_states[primary_osd]);
msgr->connect_peer(primary_osd, st_cli->peer_states[primary_osd]);
return TRY_SEND_CONNECTING;
}
}
@@ -1509,6 +1590,8 @@ int cluster_client_t::try_send(cluster_op_t *op, int i, std::function<void(osd_o
int cluster_client_t::continue_sync(cluster_op_t *op)
{
if (aborted)
return 0;
if (op->state == 1)
goto resume_1;
if (!dirty_osds.size())
@@ -1522,8 +1605,8 @@ int cluster_client_t::continue_sync(cluster_op_t *op)
for (auto do_it = dirty_osds.begin(); do_it != dirty_osds.end(); )
{
osd_num_t sync_osd = *do_it;
auto peer_it = msgr.osd_peers.find(sync_osd);
if (peer_it == msgr.osd_peers.end())
auto peer_it = msgr->osd_peers.find(sync_osd);
if (peer_it == msgr->osd_peers.end())
dirty_osds.erase(do_it++);
else
do_it++;
@@ -1578,7 +1661,7 @@ resume_1:
void cluster_client_t::send_sync(cluster_op_t *op, cluster_op_part_t *part)
{
osd_client_t *cl = msgr.osd_peers.at(part->osd_num);
osd_client_t *cl = msgr->osd_peers.at(part->osd_num);
part->flags |= PART_SENT;
op->inflight_count++;
part->op = (osd_op_t){
@@ -1595,7 +1678,7 @@ void cluster_client_t::send_sync(cluster_op_t *op, cluster_op_part_t *part)
handle_op_part(part);
},
};
msgr.outbox_push(&part->op);
msgr->outbox_push(&part->op);
}
static inline void mem_or(void *res, const void *r2, unsigned int len)
@@ -1650,7 +1733,7 @@ void cluster_client_t::handle_op_part(cluster_op_part_t *part)
reset_retry_timer(op->retry_after);
if (stop_client_id)
{
msgr.stop_client(stop_client_id);
msgr->stop_client(stop_client_id);
}
op->inflight_count--;
if (op->inflight_count == 0 && !op->retry_after)

View File

@@ -110,6 +110,7 @@ class __attribute__((visibility("default"))) cluster_client_t
{
timerfd_manager_t *tfd = NULL;
ring_loop_t *ringloop = NULL;
std::function<void(int, const std::string&)> on_fatal_error_hook;
// config:
@@ -123,6 +124,7 @@ class __attribute__((visibility("default"))) cluster_client_t
uint64_t client_max_buffered_ops = 0;
uint64_t client_max_writeback_iodepth = 0;
std::string conf_hostname;
bool exit_on_error = false;
std::string vault_url;
std::string vault_client_cert;
@@ -178,11 +180,13 @@ class __attribute__((visibility("default"))) cluster_client_t
std::multimap<osd_num_t, osd_op_t*> raw_ops;
int continuing_ops = 0;
bool msgr_initialized = false;
int aborted = 0;
std::string abort_error;
public:
std::unique_ptr<etcd_state_client_t> st_cli;
osd_messenger_t* msgr = NULL;
osd_messenger_t msgr;
void init_msgr();
json11::Json::object cli_config, file_config, etcd_global_config;
@@ -194,8 +198,10 @@ public:
void execute(cluster_op_t *op);
void execute_raw(osd_num_t osd_num, osd_op_t *op);
bool is_ready();
int is_failed(std::string & error);
void on_ready(std::function<void(void)> fn);
bool flush();
void set_on_fatal_error_hook(std::function<void(int, const std::string&)>);
bool get_immediate_commit(uint64_t inode);
@@ -204,6 +210,7 @@ public:
protected:
void continue_ops(int time_passed = 0);
void fatal_error(int errcode, const std::string & error);
std::shared_ptr<inode_cache_t> inode_cache_get(inode_t ino);
void vault_parse_config();

View File

@@ -46,6 +46,7 @@ public:
int writeback_queue_size = 0;
int writebacks_active = 0;
uint64_t last_flush_id = 0;
cluster_client_t *parent = NULL;
std::map<object_id, cluster_buffer_t> dirty_buffers;
std::vector<cluster_op_t*> writeback_overflow;

View File

@@ -301,14 +301,14 @@ int cluster_client_t::start_pg_listing(inode_list_pg_t *pg)
bool conn = true;
for (osd_num_t peer_osd: all_peers)
{
if (msgr.osd_peers.find(peer_osd) == msgr.osd_peers.end())
if (msgr->osd_peers.find(peer_osd) == msgr->osd_peers.end())
{
// Initiate connection
if (st_cli->peer_states[peer_osd].is_null())
{
return LIST_PG_WAIT_ACTIVE;
}
msgr.connect_peer(peer_osd, st_cli->peer_states[peer_osd]);
msgr->connect_peer(peer_osd, st_cli->peer_states[peer_osd]);
conn = false;
}
}
@@ -346,7 +346,7 @@ void cluster_client_t::send_list(inode_list_osd_t *cur_list)
osd_op_t *op = new osd_op_t();
op->op_type = OSD_OP_OUT;
// Already checked that it exists above, but anyway
op->client_id = msgr.osd_peers.at(cur_list->osd_num)->client_id;
op->client_id = msgr->osd_peers.at(cur_list->osd_num)->client_id;
op->req = (osd_any_op_t){
.sec_list = {
.header = {
@@ -425,7 +425,7 @@ void cluster_client_t::send_list(inode_list_osd_t *cur_list)
finish_list_pg(cur_list->pg, true);
continue_listing(cur_list->pg->lst);
};
msgr.outbox_push(op);
msgr->outbox_push(op);
}
void cluster_client_t::finish_list_pg(inode_list_pg_t *pg, bool retry_epipe)

View File

@@ -92,14 +92,19 @@ void cluster_client_t::vault_load_keys()
{
return;
}
if (vault_url.empty())
{
fatal_error(EINVAL, "Vault URL is missing");
return;
}
if (!vault_http_ctx)
{
std::string error;
vault_http_ctx = http_context_init(tfd, vault_client_cert, vault_client_key, vault_ca, true, error);
if (!vault_http_ctx)
{
fprintf(stderr, "Failed to initialize HTTP context for Vault: %s\n", error.c_str());
exit(1);
fatal_error(EINVAL, "Failed to initialize HTTP context for Vault: "+error);
return;
}
vault_http_cli = http_init(vault_http_ctx);
}

View File

@@ -336,7 +336,7 @@ void writeback_cache_t::flush_buffers(cluster_client_t *cli, dirty_buf_it_t from
{
// Buffer flushes are always retried, regardless of the error,
// so they should never result in an error here
assert(op->retval == op->len);
assert(op->retval == op->len || parent->aborted);
for (auto fl_it = flushed_buffers.lower_bound(op->flush_id);
fl_it != flushed_buffers.end() && fl_it->first == op->flush_id; )
{

View File

@@ -10,6 +10,19 @@
#include "str_util.h"
#include "json_util.h"
void etcd_state_client_t::fatal_error(int errcode, const std::string & error)
{
if (on_fatal_error_hook)
{
(std::function<void(int, const std::string&)>(on_fatal_error_hook))(errcode, error);
}
else
{
fprintf(stderr, "Error: %s\n", error.c_str());
exit(1);
}
}
etcd_state_client_t::~etcd_state_client_t()
{
for (auto watch: watches)
@@ -333,7 +346,7 @@ void etcd_state_client_t::load_pgs(std::function<void(const std::string &)> cb)
return;
}
reset_pg_exists();
etcd_watch_revision_config = etcd_watch_revision_osd = etcd_watch_revision_pg = data["header"]["revision"].uint64_value()+1;
etcd_watch_revision_config = etcd_watch_revision_osd = etcd_watch_revision_pg = data["header"]["revision"].uint64_value();
if (this->log_level > 3)
{
fprintf(stderr, "Loaded revision %ju of PG configuration\n", etcd_watch_revision_pg-1);

View File

@@ -144,6 +144,7 @@ protected:
std::set<osd_num_t> seen_peers;
bool new_pg_config = false;
void fatal_error(int errcode, const std::string & error);
void add_etcd_url(std::string);
void reset_pg_exists();
void clean_nonexistent_pgs();
@@ -192,6 +193,7 @@ public:
std::function<void()> on_reload_hook;
std::function<void(inode_t, bool)> on_inode_change_hook;
std::function<void(http_co_t *)> on_start_watcher_hook;
std::function<void(int, const std::string&)> on_fatal_error_hook;
json11::Json::object serialize_inode_cfg(inode_config_t *cfg);
inode_config_t deserialize_inode_cfg(uint64_t inode_num, json11::Json value, uint64_t mod_revision);

View File

@@ -31,6 +31,11 @@ etcd_state_client_http_t::~etcd_state_client_http_t()
tfd->clear_timer(load_pgs_timer_id);
load_pgs_timer_id = -1;
}
for (int timer_id: timers)
{
tfd->clear_timer(timer_id);
}
timers.clear();
if (http_ctx)
{
http_context_destroy(http_ctx);
@@ -49,7 +54,11 @@ void etcd_state_client_http_t::etcd_add_watch(json11::Json watch)
std::string etcd_state_client_http_t::get_username()
{
return http_context_get_ssl_cn(get_http_ctx());
if (!get_http_ctx())
{
return "";
}
return http_context_get_ssl_cn(http_ctx);
}
http_context_t *etcd_state_client_http_t::get_http_ctx()
@@ -60,8 +69,7 @@ http_context_t *etcd_state_client_http_t::get_http_ctx()
http_ctx = http_context_init(tfd, etcd_client_cert, etcd_client_key, etcd_ca, true, error);
if (!http_ctx)
{
fprintf(stderr, "Failed to initialize HTTP context: %s\n", error.c_str());
exit(1);
fatal_error(EINVAL, "Failed to initialize HTTP context: "+error);
}
}
return http_ctx;
@@ -70,7 +78,11 @@ http_context_t *etcd_state_client_http_t::get_http_ctx()
void etcd_state_client_http_t::etcd_call_oneshot(const std::string & etcd_url, const std::string & api, json11::Json payload,
int timeout, std::function<void(std::string, json11::Json)> callback)
{
auto http_cli = http_init(get_http_ctx());
if (!get_http_ctx())
{
return;
}
auto http_cli = http_init(http_ctx);
http_json_post(http_cli, etcd_url+api, payload, "", { .timeout = timeout }, [http_cli, callback](http_message_t *response)
{
std::string err;
@@ -93,7 +105,29 @@ void etcd_state_client_http_t::etcd_call(const std::string & api, json11::Json p
void etcd_state_client_http_t::etcd_call_selected(const std::string & api, json11::Json payload, int timeout,
int retries, int interval, std::function<void(std::string, json11::Json)> callback)
{
retries--;
const auto & url = selected_etcd_url;
if (url.addr.empty())
{
// Resolving failed
if (retries > 0)
{
if (log_level > 0)
{
fprintf(stderr, "Warning: etcd request failed, retrying %d more times\n", retries);
}
timers.insert(tfd->set_timer(interval < 500 ? 500 : interval, false, [this, api, payload, timeout, retries, interval, callback](int timer_id)
{
timers.erase(timer_id);
etcd_call(api, payload, timeout, retries, interval, callback);
}));
}
else
{
callback("None of etcd addresses could be resolved", json11::Json());
}
return;
}
std::string req = payload.dump();
req = "POST "+url.path+api+" HTTP/1.1\r\n"
"Host: "+url.hostname+"\r\n"
@@ -102,7 +136,6 @@ void etcd_state_client_http_t::etcd_call_selected(const std::string & api, json1
"Connection: keep-alive\r\n"
"Keep-Alive: timeout="+std::to_string(etcd_keepalive_timeout)+"\r\n"
"\r\n"+req;
retries--;
auto cb = [this, api, payload, timeout, retries, interval, callback,
cur_addr = url.addr](http_message_t *response)
{
@@ -124,11 +157,11 @@ void etcd_state_client_http_t::etcd_call_selected(const std::string & api, json1
}
if (interval > 0)
{
// FIXME: Prevent destruction of etcd_state_client if timers or requests are active
tfd->set_timer(interval, false, [this, api, payload, timeout, retries, interval, callback](int)
timers.insert(tfd->set_timer(interval, false, [this, api, payload, timeout, retries, interval, callback](int timer_id)
{
timers.erase(timer_id);
etcd_call(api, payload, timeout, retries, interval, callback);
});
}));
}
else
etcd_call(api, payload, timeout, retries, interval, callback);
@@ -140,7 +173,11 @@ void etcd_state_client_http_t::etcd_call_selected(const std::string & api, json1
callback(err, data);
};
if (!keepalive_client)
keepalive_client = http_init(get_http_ctx());
{
if (!get_http_ctx())
return;
keepalive_client = http_init(http_ctx);
}
http_request(keepalive_client, url.addr, req, { .timeout = timeout, .keepalive = true, .ssl = url.ssl }, cb);
}
@@ -153,14 +190,24 @@ void etcd_state_client_http_t::parse_config(const json11::Json & config)
stop_ws_keepalive();
start_ws_keepalive();
}
if (!etcd_addresses.size() && !etcd_local.size())
{
fatal_error(EINVAL, "etcd_address is missing in Vitastor configuration");
return;
}
get_http_ctx();
}
void etcd_state_client_http_t::pick_next_etcd(std::function<void()> cb)
{
if (!etcd_addresses.size() && !etcd_local.size())
{
fprintf(stderr, "etcd_address is missing in Vitastor configuration\n");
exit(1);
fatal_error(EINVAL, "etcd_address is missing in Vitastor configuration");
return;
}
if (!get_http_ctx())
{
return;
}
if (selected_etcd_url.addr != "")
{
@@ -196,7 +243,7 @@ void etcd_state_client_http_t::pick_next_etcd(std::function<void()> cb)
for (auto & url: etcd_name_urls)
{
resolve_count++;
http_resolve(get_http_ctx(), url.ssl, url.addr, [this, url](const std::string & error, const std::vector<std::string>& addresses)
http_resolve(http_ctx, url.ssl, url.addr, [this, url](const std::string & error, const std::vector<std::string>& addresses)
{
if (error != "")
fprintf(stderr, "Error resolving %s: %s\n", url.addr.c_str(), error.c_str());
@@ -228,8 +275,13 @@ void etcd_state_client_http_t::pick_next_etcd_on_resolve()
{
if (!etcd_urls_to_try.size())
{
fprintf(stderr, "None of etcd_address could be resolved\n");
exit(1);
selected_etcd_url = {};
auto cbs = std::move(on_resolve_queue);
for (auto cb: cbs)
{
cb();
}
return;
}
if (!rand_initialized)
{
@@ -266,6 +318,16 @@ void etcd_state_client_http_t::start_etcd_watcher_selected()
{
const auto & url = selected_etcd_url;
etcd_watches_initialised = 0;
if (url.addr.empty())
{
// Resolving failed, retry in <etcd_slow_timeout>
timers.insert(tfd->set_timer(etcd_slow_timeout, false, [this](int timer_id)
{
timers.erase(timer_id);
start_etcd_watcher();
}));
return;
}
ws_alive = 1;
if (this->log_level > 1)
{
@@ -274,7 +336,11 @@ void etcd_state_client_http_t::start_etcd_watcher_selected()
etcd_watch_revision_config, etcd_watch_revision_osd, etcd_watch_revision_pg);
}
if (!etcd_watch_ws)
etcd_watch_ws = http_init(get_http_ctx());
{
if (!get_http_ctx())
return;
etcd_watch_ws = http_init(http_ctx);
}
else
http_close(etcd_watch_ws);
open_websocket(etcd_watch_ws, url.addr, url.hostname, url.path+"/watch", { .timeout = etcd_slow_timeout, .ssl = url.ssl },
@@ -324,20 +390,17 @@ void etcd_state_client_http_t::start_etcd_watcher_selected()
etcd_watch_revision_config = etcd_watch_revision_osd = etcd_watch_revision_pg = 0;
on_reload_hook();
}
return;
}
else
{
fprintf(stderr, "Revisions before %ju were compacted by etcd, exiting\n",
data["result"]["compact_revision"].uint64_value());
exit(1);
fatal_error(EIO, "Revisions before "+data["result"]["compact_revision"].as_string()+" were compacted by etcd, exiting");
}
}
else
{
fprintf(stderr, "Watch canceled by etcd, reason: %s, exiting\n", data["result"]["cancel_reason"].string_value().c_str());
exit(1);
fatal_error(EIO, "Watch canceled by etcd, reason: "+data["result"]["cancel_reason"].string_value()+", exiting");
}
return;
}
// Save revision only if it's present in the message - because sometimes etcd sends something without a header, like:
// {"error": {"grpc_code": 14, "http_code": 503, "http_status": "Service Unavailable", "message": "error reading from server: EOF"}}
@@ -400,10 +463,11 @@ void etcd_state_client_http_t::start_etcd_watcher_selected()
if (etcd_watches_initialised == 0)
{
// Connection not established, retry in <etcd_quick_timeout>
tfd->set_timer(etcd_quick_timeout, false, [this](int)
timers.insert(tfd->set_timer(etcd_quick_timeout, false, [this](int timer_id)
{
timers.erase(timer_id);
start_etcd_watcher();
});
}));
}
else if (etcd_watches_initialised > 0)
{
@@ -492,17 +556,18 @@ void etcd_state_client_http_t::load_global_config()
{
if (err != "")
{
fprintf(stderr, "Error reading configuration from etcd: %s\n", err.c_str());
if (infinite_start)
{
tfd->set_timer(etcd_slow_timeout, false, [this](int timer_id)
fprintf(stderr, "Failed to read configuration from etcd: %s\n", err.c_str());
timers.insert(tfd->set_timer(etcd_slow_timeout, false, [this](int timer_id)
{
timers.erase(timer_id);
load_global_config();
});
}));
}
else
{
exit(1);
fatal_error(EIO, "Failed to read configuration from etcd: "+err);
}
}
});
@@ -533,10 +598,11 @@ void etcd_state_client_http_t::load_pgs()
{
// Retry indefinitely
fprintf(stderr, "Error loading PGs from etcd: %s\n", err.c_str());
tfd->set_timer(etcd_slow_timeout, false, [this](int timer_id)
timers.insert(tfd->set_timer(etcd_slow_timeout, false, [this](int timer_id)
{
timers.erase(timer_id);
load_pgs();
});
}));
}
else
{

View File

@@ -22,6 +22,7 @@ protected:
http_context_t *http_ctx = NULL;
size_t local_to_try = 0;
std::vector<http_url_t> etcd_urls_to_try;
std::set<int> timers;
http_url_t selected_etcd_url;
size_t resolve_count = 0;
std::vector<std::function<void()>> on_resolve_queue;

View File

@@ -17,6 +17,19 @@
#include "msgr_rdma.h"
#endif
void osd_messenger_t::fatal_error(int errcode, const std::string & error)
{
if (on_fatal_error_hook)
{
(std::function<void(int, const std::string&)>(on_fatal_error_hook))(errcode, error);
}
else
{
fprintf(stderr, "Error: %s\n", error.c_str());
exit(1);
}
}
void osd_messenger_t::init()
{
init_tls();
@@ -201,7 +214,7 @@ osd_messenger_t::~osd_messenger_t()
destroy_tls();
}
static int parse_proto_checksums(const json11::Json & val, int default_value)
int osd_messenger_t::parse_proto_checksums(const json11::Json & val, int default_value)
{
if (val.is_string())
{
@@ -219,9 +232,9 @@ static int parse_proto_checksums(const json11::Json & val, int default_value)
}
else if (val.is_null())
return default_value;
fprintf(stderr, "proto_checksums should be \"full\", \"payload\", \"gcm\", \"none\""
", \"\" or null (default), but it is: %s\n", val.dump().c_str());
exit(1);
fatal_error(EINVAL, "proto_checksums should be \"full\", \"payload\", \"gcm\", \"none\""
", \"\" or null (default), but it is: "+val.dump());
return default_value;
}
void osd_messenger_t::parse_config(const json11::Json & config, bool init)
@@ -813,7 +826,8 @@ void osd_messenger_t::accept_connections(int listen_fd)
}
if (peer_fd == -1 && errno != EAGAIN)
{
throw std::runtime_error(std::string("accept: ") + strerror(errno));
fprintf(stderr, "Error: accept: %s (code %d), exiting\n", strerror(errno), errno);
exit(1);
}
}

View File

@@ -259,6 +259,7 @@ protected:
bool gcm_enabled = false;
msgr_handshake_ctx_i *hs_ctx = NULL;
void fatal_error(int errcode, const std::string & error);
void init_tls();
void destroy_tls();
bool init_tls_client(osd_client_t *cl);
@@ -319,6 +320,7 @@ public:
std::function<void(osd_num_t)> break_pg_locks;
std::function<bool(osd_client_t*, json11::Json)> check_config_hook;
std::function<void(osd_client_t*)> handshake_hook;
std::function<void(int, const std::string&)> on_fatal_error_hook;
void read_requests();
void send_replies();
void accept_connections(int listen_fd);
@@ -346,7 +348,8 @@ public:
void measure_exec(osd_op_t *cur_op);
protected:
void try_connect_peer(osd_num_t osd_num);
int parse_proto_checksums(const json11::Json & val, int default_value);
void try_connect_peer(uint64_t osd_num);
void try_connect_peer_tcp(osd_num_t peer_osd, const char *peer_host, int peer_port);
void handle_peer_epoll(int peer_fd, int epoll_events);
void handle_connect_epoll(int peer_fd);

View File

@@ -597,10 +597,9 @@ void osd_messenger_t::init_tls()
if (client_cert.empty() || client_key.empty() || osd_ca.empty() || osd_num && client_ca.empty())
{
if (osd_num)
fprintf(stderr, "Vitastor transport encryption requires osd_cert, osd_pkey, osd_ca, client_ca options for OSDs\n");
fatal_error(EINVAL, "Vitastor transport encryption requires osd_cert, osd_pkey, osd_ca, client_ca options for OSDs");
else
fprintf(stderr, "Vitastor transport encryption requires cert, pkey and osd_ca options\n");
exit(1);
fatal_error(EINVAL, "Vitastor transport encryption requires cert, pkey and osd_ca options");
}
else
{
@@ -609,8 +608,8 @@ void osd_messenger_t::init_tls()
hs_ctx = msgr_handshake_ctx_i::create_ctx();
if (!hs_ctx->init(client_cert, client_key, osd_ca, client_ca, admin_ca))
{
fprintf(stderr, "Error: %s\n", hs_ctx->get_error().c_str());
exit(1);
gcm_enabled = false;
fatal_error(EINVAL, hs_ctx->get_error());
}
#endif
}

View File

@@ -575,7 +575,7 @@ json11::Json osd_messenger_t::connect_rdma(uint64_t client_id, std::string rdma_
return json11::Json();
}
static void try_send_rdma_wr(osd_client_t *cl, ibv_sge *sge, int op_sge)
static int try_send_rdma_wr(osd_client_t *cl, ibv_sge *sge, int op_sge)
{
ibv_send_wr *bad_wr = NULL;
ibv_send_wr wr = {
@@ -586,12 +586,12 @@ static void try_send_rdma_wr(osd_client_t *cl, ibv_sge *sge, int op_sge)
.send_flags = IBV_SEND_SIGNALED,
};
int err = ibv_post_send(cl->rdma_conn->qp, &wr, &bad_wr);
if (err || bad_wr)
{
fprintf(stderr, "RDMA send failed: %s (code %d)\n", strerror(err), err);
exit(1);
}
if (err)
return err;
if (bad_wr)
return EIO;
cl->rdma_conn->cur_send++;
return 0;
}
bool osd_messenger_t::try_send_rdma(osd_client_t *cl)
@@ -605,8 +605,11 @@ bool osd_messenger_t::try_send_rdma(osd_client_t *cl)
rc->send_out.mr = ibv_reg_mr(rc->ctx->pd, rc->send_out.buf, rc->send_out_size, 0);
if (!rc->send_out.mr)
{
fprintf(stderr, "Failed to register RDMA memory region: %s\n", strerror(errno));
exit(1);
rc->send_out_size = 0;
free(rc->send_out.buf);
rc->send_out.buf = NULL;
fatal_error(EIO, std::string("Failed to register RDMA memory region: ")+strerror(errno)+" (code "+std::to_string(errno)+")");
return false;
}
}
// Copy data into the buffer and send it
@@ -641,7 +644,12 @@ bool osd_messenger_t::try_send_rdma(osd_client_t *cl)
.length = (uint32_t)copied,
.lkey = rc->send_out.mr->lkey,
};
try_send_rdma_wr(cl, &sge, 1);
int err = try_send_rdma_wr(cl, &sge, 1);
if (err)
{
fatal_error(EIO, std::string("RDMA send failed: ")+strerror(err)+" (code "+std::to_string(err)+")");
return false;
}
rc->send_sizes.push_back(copied);
cl->unref_send_free(send_free_from, cl->next_send_free + cl->send_free.size()); // unref at once - everything is already copied
}
@@ -649,7 +657,7 @@ bool osd_messenger_t::try_send_rdma(osd_client_t *cl)
return true;
}
static void try_recv_rdma_wr(osd_client_t *cl, void *buf)
static int try_recv_rdma_wr(osd_client_t *cl, void *buf)
{
ibv_sge sge = {
.addr = (uintptr_t)buf,
@@ -663,12 +671,12 @@ static void try_recv_rdma_wr(osd_client_t *cl, void *buf)
.num_sge = 1,
};
int err = ibv_post_recv(cl->rdma_conn->qp, &wr, &bad_wr);
if (err || bad_wr)
{
fprintf(stderr, "RDMA receive failed: %s\n", strerror(err));
exit(1);
}
if (err)
return err;
if (bad_wr)
return EIO;
cl->rdma_conn->cur_recv++;
return 0;
}
bool osd_messenger_t::init_recv_rdma(osd_client_t *cl)
@@ -679,14 +687,21 @@ bool osd_messenger_t::init_recv_rdma(osd_client_t *cl)
rc->recv_buf.mr = ibv_reg_mr(rc->ctx->pd, rc->recv_buf.buf, rc->max_msg * rc->max_recv, IBV_ACCESS_LOCAL_WRITE);
if (!rc->recv_buf.mr)
{
fprintf(stderr, "Failed to register RDMA memory region: %s\n", strerror(errno));
exit(1);
free(rc->recv_buf.buf);
rc->recv_buf.buf = NULL;
fatal_error(EIO, std::string("Failed to register RDMA memory region: ")+strerror(errno)+" (code "+std::to_string(errno)+")");
return false;
}
for (uint32_t i = 0; i < rc->max_recv; i++)
{
uint8_t *b = rc->recv_buf.buf + i*rc->max_msg;
rc->recv_buffers.push_back(b);
try_recv_rdma_wr(cl, b);
int err = try_recv_rdma_wr(cl, b);
if (err)
{
fatal_error(EIO, std::string("RDMA receive failed: ")+strerror(err)+" (code "+std::to_string(err)+")");
return false;
}
}
return true;
}
@@ -705,8 +720,8 @@ void osd_messenger_t::handle_rdma_events(msgr_rdma_context_t *rdma_context)
}
if (ibv_req_notify_cq(rdma_context->cq, 0) != 0)
{
fprintf(stderr, "Failed to request RDMA completion notification, exiting\n");
exit(1);
fatal_error(EIO, "Failed to request RDMA completion notification");
return;
}
ibv_wc wc[RDMA_EVENTS_AT_ONCE];
int event_count;

View File

@@ -92,8 +92,8 @@ void osd_messenger_t::handle_rdmacm_events()
{
if (errno == EAGAIN || errno == EINTR)
break;
fprintf(stderr, "Failed to get RDMA-CM event: %s (code %d)\n", strerror(errno), errno);
exit(1);
fatal_error(EIO, std::string("Failed to get RDMA-CM event: ")+strerror(errno)+" (code "+std::to_string(errno)+")");
return;
}
// ...so we save a copy of all events EXCEPT connection requests, otherwise they sometimes fail with EVENT_DISCONNECT
if (ev->event == RDMA_CM_EVENT_CONNECT_REQUEST)
@@ -107,8 +107,8 @@ void osd_messenger_t::handle_rdmacm_events()
r = rdma_ack_cm_event(ev);
if (r != 0)
{
fprintf(stderr, "Failed to ack (free) RDMA-CM event: %s (code %d)\n", strerror(errno), errno);
exit(1);
fatal_error(EIO, std::string("Failed to ack (free) RDMA-CM event: ")+strerror(errno)+" (code "+std::to_string(errno)+")");
return;
}
}
for (auto & evl: events_copy)

View File

@@ -627,7 +627,8 @@ static int vitastor_file_open(BlockDriverState *bs, QDict *options, int flags, E
VitastorRPC task;
VitastorClient *client = bs->opaque;
void *image = NULL;
int64_t ret = 0;
char *errstr = NULL;
int ret = 0;
qemu_mutex_init(&client->mutex);
client->config_path = g_strdup(qdict_get_try_str(options, "config-path"));
// FIXME: Rename to etcd_address
@@ -658,6 +659,7 @@ static int vitastor_file_open(BlockDriverState *bs, QDict *options, int flags, E
strarray_push_kv(&opt, "rdma_gid_index", qdict_get_try_str(options, "rdma-gid-index"));
strarray_push_kv(&opt, "rdma_mtu", qdict_get_try_str(options, "rdma-mtu"));
strarray_push_kv(&opt, "client_writeback_allowed", (flags & BDRV_O_NOCACHE) ? "0" : "1");
strarray_push_kv(&opt, "client_exit_on_error", "0");
strarray_push_kv(&opt, "__version_check_buffer", version_buffer);
client->proxy = vitastor_c_create_uring_json(opt.items, opt.len);
client->auto_loop = 0;
@@ -670,7 +672,7 @@ static int vitastor_file_open(BlockDriverState *bs, QDict *options, int flags, E
fprintf(stderr, "vitastor: failed to create io_uring eventfd: %s\n", strerror(errno));
error_setg(errp, "failed to create io_uring eventfd");
vitastor_close(bs);
return -1;
return -EOPNOTSUPP;
}
universal_aio_set_fd_handler(client->ctx, client->uring_eventfd, vitastor_uring_handler, NULL, client);
int vitastor_version[3] = { 0 };
@@ -714,6 +716,13 @@ static int vitastor_file_open(BlockDriverState *bs, QDict *options, int flags, E
#endif
BDRV_POLL_WHILE(bs, !task.complete);
}
if ((ret = vitastor_c_is_failed(client->proxy, &errstr)))
{
error_setg(errp, errstr);
free(errstr);
vitastor_close(bs);
return -ret;
}
client->image = image;
if (client->image)
{
@@ -724,7 +733,7 @@ static int vitastor_file_open(BlockDriverState *bs, QDict *options, int flags, E
{
error_setg(errp, "image does not exist");
vitastor_close(bs);
return -1;
return -ENOENT;
}
if (!client->size)
{
@@ -747,7 +756,7 @@ static int vitastor_file_open(BlockDriverState *bs, QDict *options, int flags, E
{
error_setg(errp, "image size not specified");
vitastor_close(bs);
return -1;
return -EINVAL;
}
bs->total_sectors = client->size / BDRV_SECTOR_SIZE;
#if QEMU_VERSION_MAJOR > 5 || QEMU_VERSION_MAJOR == 5 && QEMU_VERSION_MINOR >= 1
@@ -768,7 +777,7 @@ static int vitastor_file_open(BlockDriverState *bs, QDict *options, int flags, E
qdict_del(options, "pool");
qdict_del(options, "size");
qdict_del(options, "skip-parents");
return ret;
return 0;
}
static void vitastor_close(BlockDriverState *bs)
@@ -815,6 +824,10 @@ static void vitastor_close(BlockDriverState *bs)
g_free(client->etcd_host);
if (client->etcd_prefix)
g_free(client->etcd_prefix);
if (client->file_mirror_path)
g_free(client->file_mirror_path);
if (client->rdma_device)
g_free(client->rdma_device);
if (client->image)
g_free(client->image);
free(client->last_bitmap);

View File

@@ -6,7 +6,7 @@ includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
Name: Vitastor
Description: Vitastor client library
Version: 3.2.2
Version: 3.2.3
Libs: -L${libdir} -lvitastor_client
Cflags: -I${includedir}

View File

@@ -30,6 +30,18 @@ struct vitastor_c
QEMUSetFDHandler *aio_set_fd_handler = NULL;
void *aio_ctx = NULL;
~vitastor_c()
{
if (cli)
delete cli;
if (epmgr)
delete epmgr;
else if (tfd)
delete tfd;
if (ringloop)
delete ringloop;
}
};
extern "C" {
@@ -217,13 +229,6 @@ void* vitastor_c_get_internal_client(vitastor_c *client)
void vitastor_c_destroy(vitastor_c *client)
{
delete client->cli;
if (client->epmgr)
delete client->epmgr;
else if (client->tfd)
delete client->tfd;
if (client->ringloop)
delete client->ringloop;
delete client;
}
@@ -232,6 +237,20 @@ int vitastor_c_is_ready(vitastor_c *client)
return client->cli->is_ready();
}
int vitastor_c_is_failed(vitastor_c *client, char **errstr)
{
std::string err;
int errcode = client->cli->is_failed(err);
if (errcode)
{
*errstr = (char*)malloc(err.size()+1);
if (*errstr)
memcpy(*errstr, err.c_str(), err.size()+1);
return errcode;
}
return 0;
}
void vitastor_c_on_ready(vitastor_c *client, VitastorIOHandler cb, void *opaque)
{
client->cli->on_ready([=]()

View File

@@ -53,6 +53,7 @@ vitastor_c *vitastor_c_create_epoll_json(const char **options, int options_len);
void* vitastor_c_get_internal_client(vitastor_c *client);
void vitastor_c_destroy(vitastor_c *client);
int vitastor_c_is_ready(vitastor_c *client);
int vitastor_c_is_failed(vitastor_c *client, char **errstr);
void vitastor_c_on_ready(vitastor_c *client, VitastorIOHandler cb, void *opaque);
int vitastor_c_uring_register_eventfd(vitastor_c *client);
void vitastor_c_uring_wait_ready(vitastor_c *client);

View File

@@ -56,7 +56,7 @@ public:
json11::Json parse_tags(std::string tags);
json11::Json::object format_image(const inode_config_t & cfg);
void change_parent(inode_t cur, inode_t new_parent, cli_result_t *result);
void change_parent(inode_t cur, inode_t old_parent, inode_t new_parent, cli_result_t *result);
inode_config_t* get_inode_cfg(const std::string & name);
bool check_image_perm(const inode_config_t & cfg, bool write);

View File

@@ -54,6 +54,10 @@ json11::Json::object cli_tool_t::format_image(const inode_config_t & cfg)
{
img["parent_name"] = parent_it->second.name;
}
else
{
img["parent_name"] = "Pool:"+std::to_string(INODE_POOL(cfg.parent_id))+",ID:"+std::to_string(INODE_NO_POOL(cfg.parent_id));
}
img["parent_inode_id"] = cfg.parent_id;
img["parent_inode_num"] = INODE_NO_POOL(cfg.parent_id);
img["parent_pool_id"] = (uint64_t)INODE_POOL(cfg.parent_id);
@@ -61,7 +65,7 @@ json11::Json::object cli_tool_t::format_image(const inode_config_t & cfg)
return img;
}
void cli_tool_t::change_parent(inode_t cur, inode_t new_parent, cli_result_t *result)
void cli_tool_t::change_parent(inode_t cur, inode_t old_parent, inode_t new_parent, cli_result_t *result)
{
auto cur_cfg_it = cli->st_cli->inode_config.find(cur);
if (cur_cfg_it == cli->st_cli->inode_config.end())
@@ -79,15 +83,29 @@ void cli_tool_t::change_parent(inode_t cur, inode_t new_parent, cli_result_t *re
new_cfg.parent_id = new_parent;
json11::Json::object cur_cfg_json = cli->st_cli->serialize_inode_cfg(&new_cfg);
waiting++;
json11::Json::array cmp = json11::Json::array {
json11::Json::object {
{ "target", "MOD" },
{ "key", cur_cfg_key },
{ "result", "LESS" },
{ "mod_revision", new_cfg.mod_revision+1 },
},
};
if (old_parent)
{
std::string parent_cfg_key = base64_encode(cli->st_cli->etcd_prefix+
"/config/inode/"+std::to_string(INODE_POOL(old_parent))+
"/"+std::to_string(INODE_NO_POOL(old_parent)));
auto & parent_cfg = cli->st_cli->inode_config.at(old_parent);
cmp.push_back(json11::Json::object {
{ "target", "MOD" },
{ "key", parent_cfg_key },
{ "result", "LESS" },
{ "mod_revision", parent_cfg.mod_revision+1 },
});
}
cli->st_cli->etcd_txn_slow(json11::Json::object {
{ "compare", json11::Json::array {
json11::Json::object {
{ "target", "MOD" },
{ "key", cur_cfg_key },
{ "result", "LESS" },
{ "mod_revision", new_cfg.mod_revision+1 },
},
} },
{ "compare", cmp },
{ "success", json11::Json::array {
json11::Json::object {
{ "request_put", json11::Json::object {

View File

@@ -40,6 +40,7 @@ struct image_creator_t
inode_t new_parent_id = 0;
inode_t new_id = 0, old_id = 0;
uint64_t max_id_mod_rev = 0, idx_mod_rev = 0;
uint64_t new_parent_mod_rev = 0;
inode_config_t cur_cfg;
inode_config_t new_cfg;
@@ -154,6 +155,7 @@ struct image_creator_t
if (ic.second.name == new_parent)
{
new_parent_id = ic.second.num;
new_parent_mod_rev = ic.second.mod_revision;
if (!new_pool_id)
{
new_pool_id = INODE_POOL(ic.second.num);
@@ -162,6 +164,13 @@ struct image_creator_t
{
size = ic.second.size;
}
if (ic.second.deleted && !force)
{
result = (cli_result_t){ .err = ENOENT, .text = "Layer "+ic.second.name+
" is deleted and doesn't accept new children" };
state = 100;
return;
}
}
}
if (new_parent != "" && !new_parent_id)
@@ -226,12 +235,24 @@ resume_3:
state = 100;
return;
}
if (!parent->etcd_result["succeeded"].bool_value() &&
parent->etcd_result["responses"][0]["response_range"]["kvs"].array_items().size() > 0)
if (!parent->etcd_result["succeeded"].bool_value())
{
result = (cli_result_t){ .err = EEXIST, .text = "Image "+image_name+" already exists" };
state = 100;
return;
if (parent->etcd_result["responses"][0]["response_range"]["kvs"].array_items().size() > 0)
{
result = (cli_result_t){ .err = EEXIST, .text = "Image "+image_name+" already exists" };
state = 100;
return;
}
if (new_parent_id)
{
auto kv = parent->cli->st_cli->parse_etcd_kv(parent->etcd_result["responses"][1]["response_range"]["kvs"][0]);
if (kv.value.is_null() || kv.mod_revision > new_parent_mod_rev)
{
result = (cli_result_t){ .err = EAGAIN, .text = "Parent image "+kv.value["name"].string_value()+" modified during creation" };
state = 100;
return;
}
}
}
} while (!parent->etcd_result["succeeded"].bool_value());
// Save into inode_config for library users to be able to take it from there immediately
@@ -478,6 +499,13 @@ resume_3:
size = cur_cfg.size;
}
}
if (cur_cfg.deleted && !force)
{
result = (cli_result_t){ .err = ENOENT, .text = "Layer "+cur_cfg.name+
" is deleted and doesn't accept new children" };
state = 100;
return;
}
}
void attempt_create()
@@ -613,7 +641,24 @@ resume_3:
}).dump()) },
} },
});
};
}
else if (new_parent_id)
{
auto new_parent_key = base64_encode(
parent->cli->st_cli->etcd_prefix+"/config/inode/"+
std::to_string(INODE_POOL(new_parent_id))+"/"+std::to_string(INODE_NO_POOL(new_parent_id))
);
checks.push_back(json11::Json::object {
{ "target", "MOD" },
{ "mod_revision", new_parent_mod_rev },
{ "key", new_parent_key },
});
failure.push_back(json11::Json::object {
{ "request_range", json11::Json::object {
{ "key", new_parent_key },
} },
});
}
parent->etcd_txn(json11::Json::object {
{ "compare", checks },
{ "success", success },

View File

@@ -21,6 +21,7 @@ struct snap_flattener_t
std::string top_parent_name;
inode_t target_id = 0;
inode_config_t old_cfg, old_parent_cfg;
int state = 0;
std::function<bool(cli_result_t &)> merger_cb;
cli_result_t result;
@@ -41,6 +42,7 @@ struct snap_flattener_t
state = 100;
return;
}
old_cfg = *target_cfg;
target_id = target_cfg->num;
std::vector<inode_t> chain_list;
inode_config_t *cur = target_cfg;
@@ -63,6 +65,10 @@ struct snap_flattener_t
state = 100;
return;
}
if (cur->num == target_cfg->num)
{
old_parent_cfg = it->second;
}
cur = &it->second;
chain_list.push_back(cur->num);
}
@@ -121,7 +127,24 @@ resume_1:
return;
}
// Change parent
parent->change_parent(target_id, 0, &result);
{
inode_config_t *target_cfg = parent->get_inode_cfg(target_name);
if (!target_cfg || target_cfg->mod_revision != old_cfg.mod_revision)
{
result = (cli_result_t){ .err = EAGAIN, .text = "Layer "+target_name+" modified during deletion" };
state = 100;
return;
}
auto parent_cfg_it = parent->cli->st_cli->inode_config.find(target_cfg->parent_id);
if (parent_cfg_it == parent->cli->st_cli->inode_config.end() ||
parent_cfg_it->second.mod_revision != old_parent_cfg.mod_revision)
{
result = (cli_result_t){ .err = EAGAIN, .text = "Layer "+old_parent_cfg.name+" modified during deletion" };
state = 100;
return;
}
}
parent->change_parent(target_id, old_cfg.parent_id, 0, &result);
// Wait for it to complete
state = 2;
resume_2:

View File

@@ -59,19 +59,23 @@ struct snap_remover_t
std::map<inode_t,int> sources;
std::map<inode_t,uint64_t> inode_used;
std::vector<inode_t> merge_children;
std::map<inode_t,uint64_t> merge_children; // inode => mod_revision
std::vector<inode_t> chain_list;
std::map<inode_t,int> inverse_candidates;
inode_t inverse_parent = 0, inverse_child = 0;
inode_t new_parent = 0;
int state = 0;
int current_child = 0;
std::map<inode_t,uint64_t>::iterator child_it;
std::function<bool(cli_result_t &)> cb;
std::vector<std::string> rebased_images, deleted_images;
std::vector<uint64_t> deleted_ids;
std::string inverse_child_name, inverse_parent_name;
inode_config_t inverse_child_new_cfg, inverse_parent_new_cfg;
inode_config_t merge_child_cfg;
uint64_t merge_parent_rev = 0;
uint64_t mark_del_rev = 0;
cli_result_t result;
bool is_done()
@@ -101,6 +105,8 @@ struct snap_remover_t
goto resume_9;
else if (state == 10)
goto resume_10;
else if (state == 11)
goto resume_11;
else if (state == 100)
goto resume_100;
assert(!state);
@@ -114,10 +120,23 @@ struct snap_remover_t
{
to_name = from_name;
}
// Get children to merge
get_merge_children();
// Find all images to delete
get_rm_ranks();
if (state == 100)
return;
// Mark images as deleted to prevent rebasing newer images on them
mark_deleted();
state = 8;
resume_8:
if (parent->waiting > 0)
return;
// Wait until we get all changes up to mark_del_rev from the watcher
state = 11;
resume_11:
if (parent->cli->st_cli->etcd_watch_revision_config < mark_del_rev)
return;
// Get children to merge - from this point, images shouldn't be rebased to deleted layers
get_merge_children();
// Try to select an inode for the "inverse" optimized scenario
// Read statistics from etcd to do it
read_stats();
@@ -129,16 +148,37 @@ resume_1:
return;
choose_inverse_candidate();
// Merge children one by one, except our "inverse" child
for (current_child = 0; current_child < merge_children.size(); current_child++)
for (child_it = merge_children.begin(); child_it != merge_children.end(); child_it++)
{
if (merge_children[current_child] == inverse_child)
if (child_it->first == inverse_child)
continue;
rebased_images.push_back(parent->cli->st_cli->inode_config.at(merge_children[current_child]).name);
start_merge_child(merge_children[current_child], merge_children[current_child]);
if (!recheck_cfg(child_it->first, child_it->second, &merge_child_cfg, true))
return;
if (!merge_child_cfg.num || !merge_child_cfg.parent_id)
continue;
merge_parent_rev = parent->cli->st_cli->inode_config.at(merge_child_cfg.parent_id).mod_revision;
rebased_images.push_back(merge_child_cfg.name);
start_merge_child(child_it->first, child_it->first);
resume_2:
while (!wait_result(2))
return;
parent->change_parent(merge_children[current_child], new_parent, &result);
if (!recheck_cfg(child_it->first, child_it->second, NULL, false))
return;
if (!recheck_cfg(merge_child_cfg.parent_id, merge_parent_rev, NULL, false))
return;
if (new_parent)
{
auto np_it = parent->cli->st_cli->inode_config.find(new_parent);
if (np_it == parent->cli->st_cli->inode_config.end() || np_it->second.deleted)
{
// New parent is marked as deleted => failure
result = (cli_result_t){ .err = ENOENT, .text = "Layer "+parent->cli->st_cli->inode_config
.at(new_parent).name+" is deleted and doesn't accept new children" };
state = 100;
return;
}
}
parent->change_parent(child_it->first, merge_child_cfg.parent_id, new_parent, &result);
state = 3;
resume_3:
if (parent->waiting > 0)
@@ -193,11 +233,6 @@ resume_7:
deleted_images.push_back(parent_it->second.name);
deleted_ids.push_back(chain_list[current_child]);
}
// Mark child as deleted
start_mark_deleted(chain_list[current_child]);
resume_8:
while (!wait_result(8))
return;
start_delete_source(chain_list[current_child]);
resume_9:
while (!wait_result(9))
@@ -221,6 +256,33 @@ resume_100:
return;
}
bool recheck_cfg(inode_t ino, uint64_t mod_rev, inode_config_t *copy, bool allow_enoent)
{
auto cfg_it = parent->cli->st_cli->inode_config.find(ino);
if (cfg_it == parent->cli->st_cli->inode_config.end())
{
if (!allow_enoent)
{
result = (cli_result_t){ .err = ENOENT, .text = "Inode "+std::to_string(INODE_NO_POOL(ino))+
" from pool "+std::to_string(INODE_POOL(ino))+" disappeared during deletion" };
state = 100;
return false;
}
if (copy)
*copy = {};
return true;
}
if (cfg_it->second.mod_revision != mod_rev)
{
result = (cli_result_t){ .err = EAGAIN, .text = "Layer "+cfg_it->second.name+" modified during deletion" };
state = 100;
return false;
}
if (copy)
*copy = cfg_it->second;
return true;
}
bool wait_result(int base_state)
{
if (state == 100 || !cb)
@@ -253,9 +315,8 @@ resume_100:
return obj;
}
void get_merge_children()
void get_rm_ranks()
{
// Get all children of from..to
inode_config_t *from_cfg = parent->get_inode_cfg(from_name);
if (!from_cfg)
{
@@ -307,6 +368,11 @@ resume_100:
{
sources[item] = i--;
}
}
void get_merge_children()
{
// Get all children of from..to
for (auto & ic: parent->cli->st_cli->inode_config)
{
if (!ic.second.parent_id)
@@ -322,7 +388,7 @@ resume_100:
state = 100;
return;
}
merge_children.push_back(ic.second.num);
merge_children[ic.second.num] = ic.second.mod_revision;
if (ic.second.readonly || writers_stopped)
{
inverse_candidates[ic.second.num] = it->second;
@@ -683,24 +749,55 @@ resume_100:
});
}
void start_mark_deleted(inode_t inode)
void mark_deleted()
{
auto ino_it = parent->cli->st_cli->inode_config.find(inode);
if (ino_it == parent->cli->st_cli->inode_config.end())
json11::Json::array cmp, ok;
for (current_child = 0; current_child < chain_list.size(); current_child++)
{
char buf[1024];
snprintf(buf, 1024, "Inode 0x%jx disappeared", inode);
result = (cli_result_t){ .err = EIO, .text = std::string(buf) };
state = 100;
return;
inode_t ino = chain_list[current_child];
auto & inode_cfg = parent->cli->st_cli->inode_config.at(ino);
std::string inode_cfg_key = base64_encode(
parent->cli->st_cli->etcd_prefix+
"/config/inode/"+std::to_string(INODE_POOL(ino))+
"/"+std::to_string(INODE_NO_POOL(ino))
);
inode_cfg.deleted = true;
cmp.push_back(json11::Json::object {
{ "target", "MOD" },
{ "key", inode_cfg_key },
{ "result", "LESS" },
{ "mod_revision", inode_cfg.mod_revision+1 },
});
ok.push_back(json11::Json::object {
{ "request_put", json11::Json::object {
{ "key", inode_cfg_key },
{ "value", base64_encode(json11::Json(parent->cli->st_cli->serialize_inode_cfg(&inode_cfg)).dump()) },
} },
});
}
if (ino_it->second.deleted)
parent->waiting++;
parent->cli->st_cli->etcd_txn_slow(json11::Json::object {
{ "compare", cmp },
{ "success", ok },
}, [this](std::string err, json11::Json res)
{
return;
}
cb = parent->start_modify(json11::Json::object {
{ "image", ino_it->second.name },
{ "deleted", true },
parent->waiting--;
if (err != "")
{
result = (cli_result_t){ .err = EIO, .text = "Error marking layers as deleted: "+err };
state = 100;
return;
}
if (!res["succeeded"].bool_value())
{
result = (cli_result_t){ .err = EAGAIN, .text = "Some layers between "+from_name+" and "+to_name+" were modified during deletion" };
state = 100;
return;
}
mark_del_rev = res["header"]["revision"].uint64_value();
if (parent->progress)
printf("Layers between %s and %s marked as deleted\n", from_name.c_str(), to_name.c_str());
parent->ringloop->wakeup();
});
}

View File

@@ -368,6 +368,8 @@ class osd_t
// primary ops
void autosync();
bool start_pause_sync(pg_t & pg);
void submit_pause_sync(pg_t & pg);
bool prepare_primary_rw(osd_op_t *cur_op);
void continue_primary_read(osd_op_t *cur_op);
void continue_primary_scrub(osd_op_t *cur_op);

View File

@@ -903,11 +903,20 @@ void osd_t::apply_pg_config()
}
if (currently_taken && !take)
{
// Stop this PG
stop_pg(pg_it->second);
// Stop this PG. The monitor pauses PGs of a removed pool before forgetting them
bool removed = pg_cfg.pause && !pool_cfg.exists;
if (!removed && !pg_it->second.pause_sync || !start_pause_sync(pg_it->second))
{
stop_pg(pg_it->second);
}
}
else if (take)
{
if (currently_taken)
{
// Unpaused before the stop
pg_it->second.pause_sync = 0;
}
// Take this PG
std::set<osd_num_t> all_peers;
for (osd_num_t pg_osd: pg_cfg.target_set)

View File

@@ -251,7 +251,7 @@ bool osd_t::pick_next_recovery(osd_recovery_op_t &op)
for (auto pg_it = pgs.lower_bound(recovery_last_pg); pg_it != pgs.end(); pg_it++)
{
auto & src = recovery_last_degraded ? pg_it->second.degraded_objects : pg_it->second.misplaced_objects;
if ((pg_it->second.state & mask) == check && src.size() > 0)
if ((pg_it->second.state & mask) == check && !pg_it->second.pause_sync && src.size() > 0)
{
auto pool_it = st_cli->pool_config.find(pg_it->first.pool_id);
if (pool_it != st_cli->pool_config.end() && pool_it->second.backfillfull)

View File

@@ -682,6 +682,7 @@ void osd_t::release_peering_state(pg_t & pg)
bool osd_t::stop_pg(pg_t & pg)
{
pg.pause_sync = 0;
drop_dirty_pg_connections({ .pool_id = pg.pool_id, .pg_num = pg.pg_num });
release_peering_state(pg);
if (pg.state & (PG_STOPPING | PG_OFFLINE))
@@ -794,6 +795,10 @@ void osd_t::continue_pg(pg_t & pg)
{
finish_stop_pg(pg);
}
else if (pg.pause_sync == PG_PAUSE_SYNC_WAIT && pg.inflight == 0)
{
submit_pause_sync(pg);
}
else if ((pg.state & PG_REPEERING) && pg.can_repeer())
{
start_pg_peering(pg);

View File

@@ -14,6 +14,10 @@
#define PG_EPOCH_BITS 48
// pg_t::pause_sync
#define PG_PAUSE_SYNC_WAIT 1
#define PG_PAUSE_SYNC_RUNNING 2
struct pg_obj_loc_t
{
uint64_t role;
@@ -129,6 +133,8 @@ struct pg_t
pg_flush_batch_t *flush_batch = NULL;
int inflight = 0; // including write_queue
// Pool removed: stabilize unstable writes before stopping, nobody peers a removed pool
int pause_sync = 0;
std::multimap<object_id, osd_op_t*> write_queue;
pg_osd_set_state_t* add_object_to_state(const object_id oid, const uint64_t state, const pg_osd_set_t & osd_set);

View File

@@ -57,9 +57,10 @@ bool osd_t::prepare_primary_rw(osd_op_t *cur_op)
return false;
}
}
else if (!(pg_it->second.state & PG_ACTIVE))
else if (!(pg_it->second.state & PG_ACTIVE) ||
pg_it->second.pause_sync && cur_op->req.hdr.opcode != OSD_OP_READ)
{
// This OSD is not primary for this PG or the PG is inactive
// This OSD is not primary for this PG or the PG is inactive (or about to stop)
// FIXME: Allow reads from PGs degraded under pg_minsize, but don't allow writes
finish_op(cur_op, -EPIPE);
return false;

View File

@@ -41,6 +41,69 @@ void osd_t::autosync()
}
}
// stop_pg() forgets unstable writes and leaves them to the peering of the next primary,
// but nobody peers a removed pool. So a PG of a removed pool stabilizes them first,
// while it's still active and holds its locks. Returns false if it may be stopped right away
bool osd_t::start_pause_sync(pg_t & pg)
{
if (pg.pause_sync)
{
return true;
}
if (immediate_commit == IMMEDIATE_ALL || !(pg.state & PG_ACTIVE) ||
!pg.inflight && dirty_pgs.find({ .pool_id = pg.pool_id, .pg_num = pg.pg_num }) == dirty_pgs.end())
{
return false;
}
printf("[PG %u/%u] Pool removed, stabilizing unstable writes before stopping\n", pg.pool_id, pg.pg_num);
// New writes are refused, ones in flight are waited for in continue_pg()
pg.pause_sync = PG_PAUSE_SYNC_WAIT;
continue_pg(pg);
return true;
}
void osd_t::submit_pause_sync(pg_t & pg)
{
pool_pg_num_t pg_id = { .pool_id = pg.pool_id, .pg_num = pg.pg_num };
if (!(pg.state & PG_ACTIVE) || dirty_pgs.find(pg_id) == dirty_pgs.end())
{
pg.pause_sync = 0;
stop_pg(pg);
return;
}
pg.pause_sync = PG_PAUSE_SYNC_RUNNING;
osd_op_t *op = new osd_op_t();
pending_owned_ops.insert(op);
op->op_type = OSD_OP_IN;
op->client_id = SELF_CLIENT;
op->req = (osd_any_op_t){
.sync = {
.header = {
.magic = SECONDARY_OSD_OP_MAGIC,
.id = 1,
.opcode = OSD_OP_SYNC,
},
},
};
op->callback = [this, pg_id](osd_op_t *op)
{
if (op->reply.hdr.retval < 0)
{
printf("[PG %u/%u] Failed to stabilize unstable writes before stopping: %jd (%s)\n",
pg_id.pool_id, pg_id.pg_num, -op->reply.hdr.retval, strerror(-op->reply.hdr.retval));
}
pending_owned_ops.erase(op);
delete op;
auto pg_it = pgs.find(pg_id);
if (!in_destruction && pg_it != pgs.end() && pg_it->second.pause_sync == PG_PAUSE_SYNC_RUNNING)
{
pg_it->second.pause_sync = 0;
stop_pg(pg_it->second);
}
};
exec_op(op);
}
void osd_t::finish_op(osd_op_t *cur_op, int retval)
{
inflight_ops--;

View File

@@ -129,7 +129,8 @@ int osd_t::pick_next_scrub(object_id & next_oid)
}
while (pg_it != pgs.end())
{
if ((pg_it->second.state & PG_ACTIVE) && pg_it->second.next_scrub && pg_it->second.next_scrub <= tv_now.tv_sec)
if ((pg_it->second.state & PG_ACTIVE) && !pg_it->second.pause_sync &&
pg_it->second.next_scrub && pg_it->second.next_scrub <= tv_now.tv_sec)
{
// Continue scrubbing from the next object
if (scrub_last_pg == pg_it->first)

View File

@@ -8,6 +8,12 @@
#include "messenger.h"
void osd_messenger_t::fatal_error(int retval, const std::string & error)
{
fprintf(stderr, "%s\n", error.c_str());
exit(1);
}
void osd_messenger_t::init()
{
}

View File

@@ -3719,6 +3719,197 @@ void test_replicated_add_object_to_set_degraded()
printf("test_replicated_add_object_to_set_degraded passed\n");
}
static void set_pause_ec_pool(osd_test_fixture_t & f, bool exists)
{
json11::Json::object pools;
if (exists)
{
pools["1"] = json11::Json::object {
{ "name", "pool_1" },
{ "scheme", "ec" },
{ "pg_size", 3 },
{ "pg_minsize", 2 },
{ "parity_chunks", 1 },
{ "pg_count", 1 },
{ "failure_domain", "osd" },
{ "immediate_commit", "none" },
};
}
f.st_cli->set("/vitastor/config/pools", pools);
}
// EC 2+1 pool with immediate_commit=none, one PG on OSDs 1-3 with OSD 1 primary
static void set_pause_ec_pg_config(osd_test_fixture_t & f, bool pause)
{
json11::Json::object pg_cfg = {
{ "osd_set", json11::Json::array{ 1, 2, 3 } },
{ "primary", 1 },
};
if (pause)
pg_cfg["pause"] = true;
f.st_cli->set("/vitastor/pg/config", json11::Json::object {
{ "items", json11::Json::object{ { "1", json11::Json::object { { "1", pg_cfg } } } } },
});
}
static void start_pause_ec_pg(osd_test_fixture_t & f)
{
set_pause_ec_pool(f, true);
set_pause_ec_pg_config(f, false);
f.start(json11::Json::object {
{ "osd_num", 1 },
{ "etcd_address", "127.0.0.1:2379" },
{ "immediate_commit", "none" },
{ "block_size", 131072 },
{ "bitmap_granularity", 4096 },
});
f.connect_peer(2);
f.connect_peer(3);
f.complete_peering_empty();
assert(f.pg(1, 1).state == PG_ACTIVE);
}
static void pause_ec_write_complete(osd_test_fixture_t & f);
// Full-stripe write which leaves unstable versions on OSDs 1-3. Completes it unless <complete> is false
static osd_op_t *pause_ec_write(osd_test_fixture_t & f, uint64_t client_id, bool complete = true)
{
auto *wr = make_write_op(INODE_WITH_POOL(1, 1), 0, 256*1024, 0xab);
wr->client_id = client_id;
f.exec(wr);
f.bs_zero_read_ok(0);
if (complete)
pause_ec_write_complete(f);
return wr;
}
static void pause_ec_write_complete(osd_test_fixture_t & f)
{
f.bs_write_ok(BS_OP_WRITE, 1);
for (osd_num_t p = 2; p <= 3; p++)
{
auto *pw = f.peer_take(p, OSD_OP_SEC_WRITE);
pw->reply.sec_rw.version = 1;
f.peer_complete(pw, pw->req.sec_rw.len);
}
}
// SYNC and then STABILIZE on the local store and both peers
static void pause_ec_complete_sync(osd_test_fixture_t & f)
{
for (osd_num_t p = 2; p <= 3; p++)
f.peer_complete(f.peer_take(p, OSD_OP_SEC_SYNC), 0);
auto *bs_sync = f.bs->take(BS_OP_SYNC);
bs_sync->retval = 0;
bs_sync->callback(bs_sync);
auto *bs_stab = f.bs->take(BS_OP_STABLE);
bs_stab->retval = 0;
bs_stab->callback(bs_stab);
for (osd_num_t p = 2; p <= 3; p++)
f.peer_complete(f.peer_take(p, OSD_OP_SEC_STABILIZE), 0);
}
// stop_pg() forgets unstable writes for the next primary's peering, but nobody peers a removed pool.
// A PG of a removed pool, paused by the monitor before it's forgotten, stabilizes them first,
// while it's still active, and refuses new writes meanwhile
void test_pause_stabilizes_before_stop()
{
printf("test_pause_stabilizes_before_stop\n");
osd_test_fixture_t f;
start_pause_ec_pg(f);
uint64_t client_id = f.connect_client();
pause_ec_write(f, client_id);
set_pause_ec_pool(f, false);
set_pause_ec_pg_config(f, true);
auto & pg = f.pg(1, 1);
assert(pg.state == PG_ACTIVE);
assert(pg.pause_sync == PG_PAUSE_SYNC_RUNNING);
auto *wr = make_write_op(INODE_WITH_POOL(1, 1), 0, 4096, 0xcd);
wr->client_id = client_id;
wr->req.hdr.id = 2;
f.exec(wr);
assert(wr->reply.hdr.retval == -EPIPE);
pause_ec_complete_sync(f);
f.pump();
assert(!f.has_pg(1, 1) || f.pg(1, 1).state == PG_OFFLINE);
printf("test_pause_stabilizes_before_stop passed\n");
}
void test_unpause_cancels_pause_sync()
{
printf("test_unpause_cancels_pause_sync\n");
osd_test_fixture_t f;
start_pause_ec_pg(f);
uint64_t client_id = f.connect_client();
pause_ec_write(f, client_id);
set_pause_ec_pool(f, false);
set_pause_ec_pg_config(f, true);
assert(f.pg(1, 1).pause_sync == PG_PAUSE_SYNC_RUNNING);
// Created again with the same ID
set_pause_ec_pool(f, true);
set_pause_ec_pg_config(f, false);
assert(f.pg(1, 1).pause_sync == 0);
pause_ec_complete_sync(f);
f.pump();
assert(f.pg(1, 1).state == PG_ACTIVE);
printf("test_unpause_cancels_pause_sync passed\n");
}
// A PG count change pauses PGs of an existing pool: their new primaries peer them, so they stop at once
void test_pause_of_existing_pool_stops_at_once()
{
printf("test_pause_of_existing_pool_stops_at_once\n");
osd_test_fixture_t f;
start_pause_ec_pg(f);
uint64_t client_id = f.connect_client();
pause_ec_write(f, client_id);
set_pause_ec_pg_config(f, true);
assert(!f.peer_has_sent_op(2, OSD_OP_SEC_SYNC));
f.pump();
assert(!f.has_pg(1, 1) || f.pg(1, 1).state == PG_OFFLINE);
printf("test_pause_of_existing_pool_stops_at_once passed\n");
}
// A write in flight when the PG is paused is stabilized too: the sync waits for it
void test_pause_sync_waits_for_inflight_write()
{
printf("test_pause_sync_waits_for_inflight_write\n");
osd_test_fixture_t f;
start_pause_ec_pg(f);
uint64_t client_id = f.connect_client();
auto *wr = pause_ec_write(f, client_id, false);
set_pause_ec_pool(f, false);
set_pause_ec_pg_config(f, true);
auto & pg = f.pg(1, 1);
assert(pg.state == PG_ACTIVE);
assert(pg.pause_sync == PG_PAUSE_SYNC_WAIT);
assert(!f.peer_has_sent_op(2, OSD_OP_SEC_SYNC));
pause_ec_write_complete(f);
assert(wr->reply.hdr.retval == wr->req.rw.len);
assert(pg.pause_sync == PG_PAUSE_SYNC_RUNNING);
pause_ec_complete_sync(f);
f.pump();
assert(!f.has_pg(1, 1) || f.pg(1, 1).state == PG_OFFLINE);
printf("test_pause_sync_waits_for_inflight_write passed\n");
}
int main(int narg, char *args[])
{
test_load_global_config();
@@ -3763,5 +3954,9 @@ int main(int narg, char *args[])
test_delete_waits_for_stabilize_snapshot();
test_delete_waits_only_for_covered_objects();
test_replicated_add_object_to_set_degraded();
test_pause_stabilizes_before_stop();
test_unpause_cancels_pause_sync();
test_pause_sync_waits_for_inflight_write();
test_pause_of_existing_pool_stops_at_once();
return 0;
}

View File

@@ -168,16 +168,16 @@ void pretend_disconnected(osd_messenger_t *msgr, osd_num_t osd_num)
void pretend_connected(cluster_client_t *cli, osd_num_t osd_num)
{
pretend_connected(&cli->msgr, osd_num);
pretend_connected(cli->msgr, osd_num);
}
void pretend_disconnected(cluster_client_t *cli, osd_num_t osd_num)
{
pretend_disconnected(&cli->msgr, osd_num);
pretend_disconnected(cli->msgr, osd_num);
}
#define check_disconnected(cli, osd_num) {\
if (cli->msgr.osd_peers.find(osd_num) != cli->msgr.osd_peers.end())\
if (cli->msgr->osd_peers.find(osd_num) != cli->msgr->osd_peers.end())\
{\
printf("OSD %ju not disconnected as it ought to be\n", (uint64_t)osd_num);\
assert(0);\
@@ -185,7 +185,7 @@ void pretend_disconnected(cluster_client_t *cli, osd_num_t osd_num)
}
#define check_op_count(cli, osd_num, ops) {\
osd_client_t *cl = cli->msgr.osd_peers.at(osd_num);\
osd_client_t *cl = cli->msgr->osd_peers.at(osd_num);\
int real_ops = cl->sent_ops.size();\
if (real_ops != ops)\
{\
@@ -196,7 +196,7 @@ void pretend_disconnected(cluster_client_t *cli, osd_num_t osd_num)
osd_op_t *find_op(cluster_client_t *cli, osd_num_t osd_num, uint64_t opcode, uint64_t offset, uint64_t len)
{
osd_client_t *cl = cli->msgr.osd_peers.at(osd_num);
osd_client_t *cl = cli->msgr->osd_peers.at(osd_num);
auto op_it = cl->sent_ops.begin();
while (op_it != cl->sent_ops.end())
{
@@ -225,7 +225,7 @@ void pretend_op_completed(cluster_client_t *cli, osd_op_t *op, int64_t retval)
? "sync" : (op->req.hdr.opcode == OSD_OP_WRITE ? "write" : "read"), op->req.rw.offset, op->req.rw.len);
uint64_t op_id = op->req.hdr.id;
uint64_t client_id = op->client_id;
cli->msgr.clients[client_id]->sent_ops.erase(op_id);
cli->msgr->clients[client_id]->sent_ops.erase(op_id);
op->reply.hdr.magic = SECONDARY_OSD_REPLY_MAGIC;
op->reply.hdr.id = op->req.hdr.id;
op->reply.hdr.opcode = op->req.hdr.opcode;
@@ -309,7 +309,7 @@ void test1()
uint64_t replay_start = UINT64_MAX;
uint64_t replay_end = 0;
std::vector<osd_op_t*> replay_ops;
auto osd_cl = cli->msgr.osd_peers.at(1);
auto osd_cl = cli->msgr->osd_peers.at(1);
for (auto & op_p: osd_cl->sent_ops)
{
auto op = op_p.second;
@@ -742,7 +742,7 @@ void test_deoptimize_snapshot_read()
};
cli->execute(op);
check_op_count(cli, 1, 1);
auto cl = cli->msgr.osd_peers.at(1);
auto cl = cli->msgr->osd_peers.at(1);
osd_op_t *read_op = cl->sent_ops.begin()->second;
assert(read_op->req.hdr.opcode == OSD_OP_READ);
assert(read_op->req.rw.inode == child_inode);
@@ -784,7 +784,7 @@ void test_deoptimize_snapshot_read()
};
cli->execute(op);
check_op_count(cli, 1, 1);
auto cl = cli->msgr.osd_peers.at(1);
auto cl = cli->msgr->osd_peers.at(1);
osd_op_t *read_op = cl->sent_ops.begin()->second;
assert(read_op->req.hdr.opcode == OSD_OP_READ);
assert(read_op->req.rw.inode == child_inode);
@@ -818,7 +818,7 @@ void test_deoptimize_snapshot_read()
};
cli->execute(op);
check_op_count(cli, 1, 1);
auto cl = cli->msgr.osd_peers.at(1);
auto cl = cli->msgr->osd_peers.at(1);
osd_op_t *child_read = cl->sent_ops.begin()->second;
assert(child_read->req.hdr.opcode == OSD_OP_READ);
assert(child_read->req.rw.inode == child_inode);

View File

@@ -71,6 +71,8 @@ OLD=1 ./test_move_reappear.sh
./test_rm_degraded.sh
./test_describe.sh
OLD=1 ./test_rm_pool_unstable.sh
./test_snapshot_chain.sh
SCHEME=ec ./test_snapshot_chain.sh
ENCRYPTED=1 ./test_snapshot_chain.sh
@@ -83,6 +85,8 @@ SCHEME=ec ./test_snapshot_down.sh
OLD=1 ./test_snapshot_down.sh
OLD=1 SCHEME=ec ./test_snapshot_down.sh
./test_snapshot_rm_parallel.sh
./test_kv_stress.sh
IMMEDIATE_COMMIT=1 ./test_kv_stress.sh

95
tests/test_rm_pool_unstable.sh Executable file
View File

@@ -0,0 +1,95 @@
#!/bin/bash -ex
# The primary must stabilize unstable writes of a pool being removed before its PGs stop:
# nobody peers a removed pool, so nobody else would stabilize or roll them back, and
# the old store can't trim its journal past them, so all other pools get stuck with -EAGAIN
OLD=1
SCHEME=ec
OSD_COUNT=3
PG_SIZE=3
PG_MINSIZE=2
PG_DATA_SIZE=2
OFFSET_ARGS="$OFFSET_ARGS --journal_size 16M"
# Without the autosync timer, writes stay unstable until the pool is removed.
# The other autosync limit is journal_size/block_size/2 = 64 writes on a primary
GLOBAL_CONFIG="$GLOBAL_CONFIG"',"autosync_interval":0,"client_enable_writeback":false'
. `dirname $0`/run_3osds.sh
# A primary stabilizes all its unstable writes on any SYNC, so the removed pools
# and the remaining one need different primaries
$ETCDCTL put /vitastor/config/osd/1 '{"tags":["other"]}'
$ETCDCTL put /vitastor/config/osd/2 '{"tags":["removed"]}'
$VITASTOR_CLI modify-pool testpool --primary_affinity_tags removed
$VITASTOR_CLI create-pool testpool2 -s 3 -n 1 --failure_domain osd --primary_affinity_tags other
pool_primary()
{
$ETCDCTL get /vitastor/pg/config --print-value-only | jq -r '.items["'$1'"]["1"].primary'
}
check_primaries()
{
wait_pool_up 60 $1 3 1
wait_pool_up 60 2 3 1
if [[ $(pool_primary $1) != 2 || $(pool_primary 2) != 1 ]]; then
format_error "Primaries are not where the test needs them"
fi
}
# Number of unstable writes of inode 1 of the given pool on all OSDs
count_unstable()
{
local pool=$1
local inode=$(printf 0x%x $(((pool << 48) | 1)))
for i in $(seq 1 $OSD_COUNT); do
local offsets=$(build/src/disk_tool/vitastor-disk simple-offsets --format json ./testdata/bin/test_osd$i.bin $OFFSET_ARGS)
build/src/disk_tool/vitastor-disk dump-journal --io cached --json ./testdata/bin/test_osd$i.bin 4096 \
$(echo "$offsets" | jq -r '"\(.journal_offset) \(.meta_offset-.journal_offset)"') | \
jq --arg inode $inode '[ .[] | select(.inode == $inode) ] |
([ .[] | select(.type == "stable") | {stripe, ver: (.ver|tonumber)} ]) as $stable |
([ .[] | select(.type == "rollback") | {stripe, ver: (.ver|tonumber)} ]) as $rollback |
[ .[] | select(.type == "big_write" or .type == "small_write") | .stripe as $s | (.ver|tonumber) as $v |
select(([ $stable[] | select(.stripe == $s and .ver >= $v) ] | length) == 0 and
([ $rollback[] | select(.stripe == $s and .ver < $v) ] | length) == 0) ] | length'
done | jq -s add
}
write_unstable()
{
local pool=$1
# 8 full stripes per pass, both passes together stay below the autosync limit
if ! timeout -k 10 60 $VITASTOR_FIO -bs=256k -direct=1 -iodepth=1 -rw=write -pool=$pool -inode=1 -size=2M -end_fsync=0; then
format_error "Writes to pool $pool got stuck"
fi
# A sync from another pool syncs the whole store, which puts these writes into the journal
$VITASTOR_FIO -bs=4k -direct=1 -iodepth=1 -rw=write -pool=2 -inode=1 -size=4k -end_fsync=1
if [[ $(count_unstable $pool) -eq 0 ]]; then
format_error "No unstable writes of pool $pool on OSDs, the test checks nothing"
fi
}
# Writes 4x the journal size, so the journal has to be trimmed past the removed pool's writes
write_other_pool()
{
if ! timeout -k 10 60 $VITASTOR_FIO -bs=4k -direct=1 -iodepth=16 -fsync=32 -rw=randwrite \
-pool=2 -inode=1 -size=128M -number_ios=16384 -randrepeat=0; then
format_error "$1: writes to another pool got stuck"
fi
}
check_rolled_back()
{
if [[ $(count_unstable $1) -ne 0 ]]; then
format_error "Unstable writes of removed pool $1 are still on OSDs"
fi
}
check_primaries 1
write_unstable 1
$VITASTOR_CLI rm-pool testpool
write_other_pool "Pool removed"
check_rolled_back 1
format_green OK

View File

@@ -0,0 +1,43 @@
#!/bin/bash -ex
. `dirname $0`/run_3osds.sh
check_qemu
# Test parallel rm of two snapshots
$VITASTOR_CLI create -s 32M repro-golden
$VITASTOR_FIO -bs=1M -direct=1 -iodepth=4 -end_fsync=1 -rw=write \
-image=repro-golden -mirror_file=./testdata/bin/mirror.bin
qemu-img convert -p \
-f raw "vitastor:config_path=$VITASTOR_CFG:image=repro-golden" \
-O raw ./testdata/bin/res.bin
cmp ./testdata/bin/mirror.bin ./testdata/bin/res.bin
$VITASTOR_CLI snap-create repro-golden@init
for i in {1..10}; do
$VITASTOR_CLI ls --tree
$VITASTOR_CLI snap-create repro-golden@a$i
$VITASTOR_CLI snap-create repro-golden@b$i
$VITASTOR_CLI create --parent repro-golden@a$i -s 32M repro-cl-a$i
$VITASTOR_CLI create --parent repro-golden@b$i -s 32M repro-cl-b$i
($VITASTOR_CLI rm repro-golden@a$i &>./testdata/rm_a$i.log || true) & # two concurrent rm's
CLI1=$!
($VITASTOR_CLI rm repro-golden@b$i &>./testdata/rm_b$i.log || true) &
CLI2=$!
wait $CLI1 $CLI2
qemu-img convert -p \
-f raw "vitastor:config_path=$VITASTOR_CFG:image=repro-golden" \
-O raw ./testdata/bin/res.bin
if ! cmp ./testdata/bin/mirror.bin ./testdata/bin/res.bin; then
sleep 100000
fi
done
$VITASTOR_CLI ls --tree
$VITASTOR_CLI rm --matching repro-golden@a* repro-golden@b*
$VITASTOR_CLI ls --tree
format_green OK