3131#include " paimon/core/operation/file_system_write_restore.h"
3232#include " paimon/core/operation/metrics/compaction_metrics.h"
3333#include " paimon/core/operation/restore_files.h"
34+ #include " paimon/core/realtime/realtime_context_impl.h"
3435#include " paimon/core/realtime/realtime_schema_layout.h"
3536#include " paimon/core/schema/table_schema.h"
3637#include " paimon/core/snapshot.h"
@@ -97,6 +98,7 @@ Status AbstractFileStoreWrite::Write(std::unique_ptr<RecordBatch>&& batch) {
9798 if (PAIMON_UNLIKELY (batch == nullptr )) {
9899 return Status::Invalid (" batch is null pointer" );
99100 }
101+ PAIMON_RETURN_NOT_OK (CheckRealtimeWriteUsable ());
100102 // in FileStoreWrite::Create() we have checked the table kind and bucket mode, here we only
101103 // check the bucket id in batch
102104 if (options_.GetBucket () == -1 ) {
@@ -149,6 +151,8 @@ Status AbstractFileStoreWrite::Seal() {
149151 if (!IsRealtimeWrite ()) {
150152 return FileStoreWrite::Seal ();
151153 }
154+ std::lock_guard<std::mutex> seal_prepare_lock (realtime_seal_prepare_mutex_);
155+ PAIMON_RETURN_NOT_OK (CheckRealtimeWriteUsable ());
152156 std::vector<std::shared_ptr<BatchWriter>> writers;
153157 {
154158 std::lock_guard<std::mutex> lock (writers_mutex_);
@@ -159,13 +163,18 @@ Status AbstractFileStoreWrite::Seal() {
159163 }
160164 }
161165 for (const std::shared_ptr<BatchWriter>& writer : writers) {
162- PAIMON_RETURN_NOT_OK (writer->Seal ());
166+ Status status = writer->Seal ();
167+ if (!status.ok ()) {
168+ // Earlier buckets may already be sealed, so the writer cannot safely continue.
169+ return PoisonRealtimeWrite (status);
170+ }
163171 }
164172 return Status::OK ();
165173}
166174
167175Status AbstractFileStoreWrite::Compact (const std::map<std::string, std::string>& partition,
168176 int32_t bucket, bool full_compaction) {
177+ PAIMON_RETURN_NOT_OK (CheckRealtimeWriteUsable ());
169178 PAIMON_ASSIGN_OR_RAISE (BinaryRow part, file_store_path_factory_->ToBinaryRow (partition));
170179 PAIMON_ASSIGN_OR_RAISE (std::shared_ptr<BatchWriter> writer, GetWriter (part, bucket));
171180 assert (writer);
@@ -300,10 +309,18 @@ Result<std::vector<RealtimeCommitProgress>> AbstractFileStoreWrite::PrepareCommi
300309 if (is_streaming_mode_ == false ) {
301310 return Status::Invalid (" PrepareCommitWithProgress requires streaming mode" );
302311 }
312+ // Keep one seal/prepare attempt in flight so a failure fences every later checkpoint.
313+ std::lock_guard<std::mutex> seal_prepare_lock (realtime_seal_prepare_mutex_);
314+ PAIMON_RETURN_NOT_OK (CheckRealtimeWriteUsable ());
303315 // Real-time prepare snapshots writers under a short lock so writes can continue while sealed
304316 // segments are flushed. The normal path iterates and may erase writers in place, which would
305317 // either race with concurrent writer creation or hold writers_mutex_ for the whole prepare.
306- return PrepareRealtimeCommit ();
318+ Result<std::vector<RealtimeCommitProgress>> result = PrepareRealtimeCommit ();
319+ if (!result.ok ()) {
320+ // Prepare may have drained an earlier bucket or partially written the failing bucket.
321+ return PoisonRealtimeWrite (result.status ());
322+ }
323+ return result;
307324}
308325
309326Result<std::vector<RealtimeCommitProgress>> AbstractFileStoreWrite::PrepareRealtimeCommit () {
@@ -362,22 +379,91 @@ Result<std::vector<RealtimeCommitProgress>> AbstractFileStoreWrite::PrepareRealt
362379}
363380
364381Status AbstractFileStoreWrite::Close () {
382+ std::unique_lock<std::mutex> seal_prepare_lock (realtime_seal_prepare_mutex_, std::defer_lock);
383+ if (IsRealtimeWrite ()) {
384+ seal_prepare_lock.lock ();
385+ }
365386 std::lock_guard<std::mutex> lock (writers_mutex_);
387+ bool has_unprepared_realtime_data = false ;
388+ Status unprepared_error = Status::OK ();
389+ if (IsRealtimeWrite ()) {
390+ for (const auto & [_, bucket_writers] : writers_) {
391+ for (const auto & [_, writer_container] : bucket_writers) {
392+ if (writer_container.writer ->HasUnpreparedRealtimeData ()) {
393+ has_unprepared_realtime_data = true ;
394+ break ;
395+ }
396+ }
397+ if (has_unprepared_realtime_data) {
398+ break ;
399+ }
400+ }
401+ if (has_unprepared_realtime_data) {
402+ // Close cannot publish pending segments for its caller.
403+ unprepared_error = PoisonRealtimeWrite (
404+ Status::Invalid (" real-time writer closed with data not covered by a successful "
405+ " PrepareCommitWithProgress" ));
406+ }
407+ }
408+ Status first_error = Status::OK ();
366409 for (auto & [_, bucket_writers] : writers_) {
367410 for (auto & [_, writer_container] : bucket_writers) {
368411 writer_memory_manager_->UnregisterWriter (writer_container.writer .get ());
369- PAIMON_RETURN_NOT_OK (writer_container.writer ->Close ());
412+ Status status = writer_container.writer ->Close ();
413+ if (!status.ok () && first_error.ok ()) {
414+ first_error = std::move (status);
415+ }
370416 }
371417 }
372418 writers_.clear ();
373419 compact_executor_->ShutdownNow ();
374- return Status::OK ();
420+ if (!first_error.ok () && IsRealtimeWrite ()) {
421+ return PoisonRealtimeWrite (first_error);
422+ }
423+ if (has_unprepared_realtime_data) {
424+ return unprepared_error;
425+ }
426+ return first_error;
375427}
376428
377429std::shared_ptr<Metrics> AbstractFileStoreWrite::GetMetrics () const {
378430 return metrics_;
379431}
380432
433+ Status AbstractFileStoreWrite::CheckRealtimeWriteUsable () const {
434+ if (IsRealtimeWrite () && realtime_write_poisoned_.load ()) {
435+ return Status::Invalid (
436+ " real-time writer cannot be reused after Seal or PrepareCommitWithProgress failed, "
437+ " or after it closed with unprepared data; recreate RealtimeContext and "
438+ " FileStoreWrite, then let upstream recover input from the durable recovery offset "
439+ " persisted in the snapshot" );
440+ }
441+ std::shared_ptr<RealtimeContext> context = GetRealtimeContext ();
442+ if (!context) {
443+ return Status::OK ();
444+ }
445+ PAIMON_ASSIGN_OR_RAISE (std::shared_ptr<RealtimeContextImpl> context_impl,
446+ RealtimeContextImpl::Cast (context));
447+ return context_impl->CheckUsable ();
448+ }
449+
450+ void AbstractFileStoreWrite::InvalidateRealtimeContext () {
451+ std::shared_ptr<RealtimeContextImpl> context =
452+ std::dynamic_pointer_cast<RealtimeContextImpl>(GetRealtimeContext ());
453+ if (context) {
454+ context->Invalidate ();
455+ }
456+ }
457+
458+ Status AbstractFileStoreWrite::PoisonRealtimeWrite (const Status& cause) {
459+ realtime_write_poisoned_.store (true );
460+ InvalidateRealtimeContext ();
461+ return cause.WithMessage (cause.message (),
462+ " ; recreate RealtimeContext and FileStoreWrite, then let upstream "
463+ " recover input from the durable recovery offset persisted in the "
464+ " snapshot" );
465+ }
466+
381467int32_t AbstractFileStoreWrite::GetDefaultBucketNum () const {
382468 return options_.GetBucket ();
383469}
0 commit comments