Releases: anthonyeleven/vcephfs_transcoder
Release list
2061
Re-pointed at main on 2026-09-22. The version name is held at 2061 deliberately — it names the release, not each change set — so this release moves forward rather than the number going up.
What is in it
Apply the regulator's config keys
The regulator's keys were added to RuntimeConfig.KEYS and to the parser, but
_apply_config only copied four of them onto args. The two that decide whether
the regulator runs at all -- regulate_prometheus_url and regulate_query -- were
parsed, validated, and then discarded.
The failure is silent and looks exactly like success. A fully configured job
applies its config with no complaint and, one millisecond later, logs
"Self-regulation disabled (no regulate_prometheus_url)" and runs unthrottled.
regulate_floor_ms had the mirror-image gap: a REG_DEFAULTS entry and a
--regulate-floor-ms flag, but no place in KEYS, so setting it in a config file
was rejected as an unknown key.
- Route every regulate_* key through one module-level REGULATE_APPLY table,
and test as an invariant that it matches the regulate_* keys the parser
accepts. Either half alone silently does nothing. - Accept and range-check regulate_floor_ms from the config file.
- Warn when regulate_prometheus_url or regulate_query is edited on a running
job: the regulator is constructed once, so a later change is accepted and
has no effect. - Add Regulator.set_floor_base(), since the decay baseline is captured at
construction and a config change would otherwise be ignored. - Document all seven keys in config_example(), which --help prints; none of
them appeared there. The regulator defaults move up beside the other
constants so the example interpolates them rather than retyping values
that drift.
Seven tests. One is a control that removes a key from the routing table and
asserts the invariant notices, so the check cannot pass vacuously -- verified
by reintroducing the original defect, which fails two tests.
Assisted-by: Claude Opus 5
Escape the volume for PromQL, not just for regex
Substituting {volume} used re.escape() alone. That is right for the regex but
not for the string literal the regex sits inside: PromQL uses Go escaping,
where . is not a valid escape sequence.
ceph_daemon=~"mds.myvol..*"
-> HTTP 400: parse error: unknown escape sequence U+002E '.'
Every poll fails identically, so the regulator disables itself at startup with
"the query did not return a usable value" and the job runs unthrottled. A wrong
match would at least have regulated against something; this regulates against
nothing.
Double the backslashes re.escape() produces, and say so in config_example(),
since the operator writes the rest of the query by hand and meets the same
trap. Confirmed against a live Prometheus: the single-backslash form 400s, the
doubled form returns one series.
Assisted-by: Claude Opus 5
Bump version to 2010
The regulator did not work in 2001: its config keys were never applied, and
{volume} was not escaped for PromQL. Anything reporting 2001 in the field
should be treated as running unregulated.
Assisted-by: Claude Opus 5
Add --paths-from for targeted second passes
A second pass at a lower --min-size already knows its candidates: the previous
run logged every file it skipped and why. Rediscovering them by walking is the
expensive part, because file_delay sleeps once per file ENCOUNTERED in the
single-threaded producer loop, before the stat -- so a pass costs roughly
rfiles x delay however few files qualify. On one production volume that meant
visiting 195M paths to reach 8.7M candidates: about 45 days of walking to do
2 days of work.
--paths-from FILE takes the list instead ('-' for stdin), NUL- or
newline-delimited, detected from the content and read in chunks rather than
slurped, since these lists run to tens of millions of lines. The directories are
still required: they are not scanned, they bound what the list may touch, and a
Tests
Suite is at 107 tests, up from 75 at the 2061 tag's original cut. Each change set was run against the unpatched script as well as the patched one, so the new tests are known to fail without their fix rather than merely passing alongside it.
2001
Brings the tool up to the currently deployed build, and folds self-regulation in so there is no longer a second script to run alongside it.
copy_file_range is now off by default
--copy-file-range opts back in. A size sweep to 200 GiB found a fixed per-call cost of roughly 9 ms, which at 256 KiB is the entire transfer — 27x slower than a plain userspace copy — and amortizes away by 4 MiB.
That default suits a workload whose large files have already migrated and whose remainder is a small-file tail. It may be wrong for you: on CephFS copy_file_range can become an OSD-to-OSD copy that never crosses the client's link, which matters far more than per-call overhead for a client on a slow or distant path. Measurement here showed both methods moving 2.02x filesize across the client link, so no offload was happening, but that is one configuration. The README explains how to measure it yourself.
Self-regulation, folded in
The transcoder can now throttle itself against a latency signal it could not otherwise see: what the filesystem's real clients experience, rather than what the transcoder experiences. Previously a separate process drove it over signals and grepped its log to confirm each one landed.
Configured with a complete PromQL expression, contract being one value in milliseconds:
regulate_prometheus_url = https://prometheus.example.net/api/v1/query
regulate_query = 1e3 * sum(increase(mds_latency_sum{...}[1m])) / sum(increase(mds_latency_count{...}[1m]))
regulate_pause_ms = 150
Nothing assumes Ceph's exporter, or Ceph. {volume} is optionally substituted from mds_namespace and regex-escaped. Fully usable with no Prometheus — leave the URL unset and the thread never starts. A query that fails, returns nothing, or returns more than one series holds current settings rather than adjusting, because a monitoring outage is not evidence about the filesystem. Units are checked at startup and called out, since seconds instead of milliseconds fails silently in both directions.
Other changes
- Per-path replace lock.
replace_lockwas a module-global mutex around the whole stat/rename/utime sequence, so one worker process-wide could be in the MDS-bound phase at a time — measured effective concurrency 0.96 on a 15-thread job. Now striped by path hash, preserving the per-path guarantee. --source-poolrestricts a run to files currently in one pool, so draining one pool into another does not also sweep up everything on the default pool.--prune-small-subtreesskips subtrees using the MDS's ownceph.dir.rbytes/rfiles— onegetfattrin place of a walk. The rbytes ceiling is the safety property, since a total bounds the loss however the bytes are distributed where a mean cannot.--prune-budget-bytescaps the aggregate. Off by default.--helpno longer crashes. argparse renders the epilog throughtext % dict(prog=...), and the epilog embeds the config example, one of whose comments contained a literal%.- Crossover advisory. Warns when
--min-sizesits below the size at which the move starts saving space. The source scheme is a parameter, not an assumption — comparing against 3x replication only is wrong when the source is R2 or another EC profile. mds_namespace, not the path. A mount point's name is not necessarily its filesystem's name. Autofs is triggered before reading, sinceos.statalone does not mount.
Documentation and tests
README now documents the runtime config file, including that an edit re-applies every key present in it — which has already reset a running job's thread count. Also carries a comparison against RADOS-layer pool migration, and what transcoding costs in object count rather than bytes.
45 unit tests, up from none. Two are deliberately controls that fail against code which does nothing, so the rest cannot pass vacuously.