Skip to content

feat(cpp): append to a TsFile that was closed normally - #980

Open
Adarsh-Me wants to merge 1 commit into
apache:developfrom
Adarsh-Me:feat/tsfile-923-open-for-append
Open

Adarsh-Me wants to merge 1 commit into
apache:developfrom
Adarsh-Me:feat/tsfile-923-open-for-append

Conversation

@Adarsh-Me

Copy link
Copy Markdown

What this does

Adds RestorableTsFileIOWriter::open_for_append(path), which makes a TsFile that was closed normally writable, as discussed in #923.

The metadata tail is located from the footer's own length (size - (magic + i32) - meta_size - 1) and removed, then the schema and chunk metadata are rebuilt from the chunks underneath, so writing continues where the file stopped and close() regenerates the tail. Per review on the issue, the byte at the boundary is not inspected — Java's truncate(fileMetadataPos - 1) does not require a marker either, and the fixtures in this repo carry MetaMarker (0x03) where current writers put SEPARATOR_MARKER (0x02), so a marker check here would reject files this library is happy to append to.

open() is unchanged: a complete file still reports can_write() == false. A file that never got a footer is recovered exactly as open(path, true) recovers it. Scope stays V4-only.

Why a scan and not the footer

TsFileWriter::init(RestorableTsFileIOWriter*) already consumes what the chunk scan produces — real device ids, per-measurement encoding/compression, recovered statistics, aligned_devices_, table schemas via update_table_schema()/finalize_table_schemas(). Reusing it means appending inherits the existing recovery tests instead of a second recovery path over index-node structures that would also have to re-derive per-chunk statistics from the footer.

The one rule that is not in the crash path

The tail is only removed once every chunk under it has been read back:

  • scan limit is the metadata offset, not the end of the file;
  • if the scan stops short of it, the call fails and the file is left byte-identical.

Crash recovery truncating at the last parseable offset is correct for a half-written file, but here the file is known-good, so partial progress would truncate data that is still valid — append would destroy a file it should have refused. For the same reason a chunk whose body cannot be read is an error in this mode rather than a chunk recorded with empty statistics: those statistics are what the regenerated footer will claim the file holds.

Tests

cpp/test/file/restorable_tsfile_append_test.cc: append to a closed file (tree and table model) and read old + new rows back; new device and new measurement after recovery; older-than-recovered timestamp refused; no-op append+close compared against time-windowed queries taken before it, since the regenerated tail is rebuilt from scanned chunk metadata rather than copied; four append/close cycles, for the OPERATION_INDEX_RANGE segment each close() writes ahead of its separator; bad metadata length and non-TsFile bytes both refused with the file unchanged; incomplete file salvaged then appended; empty and header-only files; open() still refusing a complete file; second open on a live handle refused.

What I ran, and what I did not

Ran: ./mvnw spotless:check -P with-cpp (BUILD SUCCESS, with clang-format 17.0.6, the version this repo pins) and g++ -fsyntax-only -std=c++11 -Wall over restorable_tsfile_io_writer.cc, tsfile_writer.cc, tsfile_tree_writer.cc, tsfile_table_writer.cc — clean, no new warnings (the one -Wsign-compare in the file is pre-existing at the same line upstream).

Not run: the gtest suite itself. This machine has no CMake and no usable container/WSL toolchain, and the single MinGW available (6.3.0, win32 threads) cannot even parse std::mutex in tsfile_io_reader.h, so the test binary could not be configured, let alone executed. No behaviour in this PR was observed running. That is what Unit-Test-Cpp will establish, and I will fix whatever it reports.

Not in this PR

The C entry points (tsfile_writer_append_new() and friends) and their lifetime handling. They need a non-truncating internal open — write_file_new() uses O_RDWR | O_CREAT | O_TRUNC today, which is the data-loss trap behind requirement 2 of #923 — and the recovered RestorableTsFileIOWriter has to outlive the higher-level writer behind a single C handle. I would rather land the C++ behaviour first and send that as a second PR with its own wrapper tests.

Closes #923 once the C side follows.

A TsFile that was closed normally is not writable, and every C entry point
that could make it writable truncates the file on open, so appending to
existing data discards it. Add open_for_append(), which locates the metadata
tail from the footer's own length, removes it, and rebuilds the schema from
the chunks underneath, so writing continues and close() regenerates the tail.

The tail is only removed once every chunk under it has been read back: a
region the scan cannot consume is refused with the file byte-identical,
because partial progress here would truncate data that is still valid. A file
that never got a footer is recovered the way open() already recovers it.

Aligned with apache#923.
@Adarsh-Me

Copy link
Copy Markdown
Author

Heads-up on CI: all 9 workflow runs for this head are sitting at action_required, so nothing has compiled anywhere yet (fork PR, my first PR here). Could someone with write access approve the runs? The C++ suite can't be configured on my machine (no CMake), so Unit-Test-Cpp is the only execution evidence this PR will have. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Support appending to a normally closed TsFile in C++ and C APIs

1 participant