spark-test commented on code in PR #59726:
URL: https://github.com/apache/spark/pull/59726#discussion_r4354464167


##########
examples/src/main/cpp/native-data-sink/json_sink.cc:
##########
@@ -0,0 +1,573 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "json_sink.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <atomic>
+#include <cerrno>
+#include <chrono>
+#include <cstring>
+#include <fstream>
+#include <set>
+#include <sstream>
+#include <system_error>
+
+#include <arrow/api.h>
+
+namespace cpp_sdk_sink {
+
+namespace fs = std::filesystem;
+using spark::native::Capabilities;
+using spark::native::CreateInfo;
+using spark::native::WriteMode;
+
+namespace {
+
+std::atomic<uint64_t> next_id{0};
+
+arrow::Status Error(const std::string& action, const fs::path& path, 
std::error_code error) {
+  return arrow::Status::IOError(action, " ", path.string(), ": ", 
error.message());
+}
+
+arrow::Status ErrnoError(const std::string& action, const fs::path& path) {
+  return Error(action, path, std::error_code(errno, std::generic_category()));
+}
+
+arrow::Status ValidateSchema(const arrow::Schema& schema) {
+  if (schema.num_fields() != 2 || schema.field(0)->name() != "id" ||
+      !schema.field(0)->type()->Equals(*arrow::int64()) || 
schema.field(1)->name() != "label" ||
+      !schema.field(1)->type()->Equals(*arrow::utf8())) {
+    return arrow::Status::Invalid("Expected columns id BIGINT, label STRING, 
in that order");
+  }
+  return arrow::Status::OK();
+}
+
+std::string UniqueId() {
+  auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(
+                   std::chrono::system_clock::now().time_since_epoch())
+                   .count();
+  return std::to_string(::getpid()) + "-" + std::to_string(nanos) + "-" +
+         std::to_string(next_id.fetch_add(1));
+}
+
+arrow::Result<fs::path> UniqueDirectory(const fs::path& parent) {
+  for (int attempt = 0; attempt < 16; ++attempt) {
+    fs::path path = parent / UniqueId();
+    std::error_code error;
+    if (fs::create_directory(path, error)) {
+      return path;
+    }
+    if (error) {
+      return Error("Cannot create", path, error);
+    }
+  }
+  return arrow::Status::IOError("Could not create a unique write operation 
directory");
+}
+
+/// Whether `name` is the name of a file in a directory, without other path 
components.
+bool IsFilename(const std::string& name) {
+  fs::path path(name);
+  return !name.empty() && name.find('/') == std::string::npos && name != "." 
&& name != ".." &&
+         path.filename() == path;
+}
+
+arrow::Status WriteAll(int descriptor, std::string_view data, const fs::path& 
path) {
+  while (!data.empty()) {
+    ssize_t written = ::write(descriptor, data.data(), data.size());
+    if (written < 0) {
+      if (errno == EINTR) {
+        continue;
+      }
+      return ErrnoError("Cannot write", path);
+    }
+    data.remove_prefix(static_cast<size_t>(written));
+  }
+  return arrow::Status::OK();
+}
+
+/// Creates a file that does not exist yet with `contents`, and syncs it.
+arrow::Status WriteNewFile(const fs::path& path, const std::string& contents) {
+  int descriptor = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | 
O_CLOEXEC, 0644);
+  if (descriptor < 0) {
+    return ErrnoError("Cannot create", path);
+  }
+  arrow::Status status = WriteAll(descriptor, contents, path);
+  if (status.ok() && ::fsync(descriptor) != 0) {
+    status = ErrnoError("Cannot sync", path);
+  }
+  if (::close(descriptor) != 0 && status.ok()) {
+    status = ErrnoError("Cannot close", path);
+  }
+  return status;
+}
+
+nlohmann::json ManifestJson(const Manifest& manifest) {
+  nlohmann::json json = {{"version", manifest.version}, {"files", 
manifest.files}};
+  if (!manifest.epochs.empty()) {
+    json["epochs"] = manifest.epochs;
+  }
+  return json;
+}
+
+/// The writer of one task attempt, which stages its rows as JSON lines in a 
file of its own.
+class JsonTaskWriter : public spark::native::DataWriter<CommitMessage> {
+ public:
+  JsonTaskWriter(spark::native::TaskContext task, fs::path path, CommitMessage 
message,
+                 int descriptor)
+      : task_(task),
+        path_(std::move(path)),
+        message_(std::move(message)),
+        descriptor_(descriptor) {}
+
+  JsonTaskWriter(const JsonTaskWriter&) = delete;
+  JsonTaskWriter& operator=(const JsonTaskWriter&) = delete;
+
+  ~JsonTaskWriter() override {
+    // A committed file belongs to the driver, which publishes or removes it.
+    if (!committed_) {
+      Discard();
+      ::unlink(path_.c_str());
+    }
+  }
+
+  // The SDK never writes after a failure, commit, or abort, commits at most 
once, and never

Review Comment:
   **Nit (P3):** This contradicts the DataWriter lifecycle in `data_source.h`: 
Abort "may follow a failed Write or Commit, including a Commit that returned a 
message that the SDK could not serialize". In `internal/bridge.h`, 
`CommitWriter()` calls `writer_->Commit()` and then `Encode(message)`. If 
`Encode` fails, `TaskWriter` is left in `kFailed`, and the next `Abort()` 
reaches `JsonTaskWriter::Abort()` after a successful `Commit()`. This writer 
handles that correctly, because `Abort()` unlinks the file unconditionally. But 
this is the reference connector, and an author who relies on the comment's 
stated guarantee could write an Abort that skips cleanup once the output is 
committed. That would leak staged data when message serialization fails. 
Consider saying that the SDK may abort after a Commit whose message it could 
not serialize, and that `Abort()` removes the file in every case, so no phase 
tracking is needed.
   
   **Verification:**
   - **Inspection:** The comment describing the SDK guarantees for the example 
task writer states that Abort may follow a Commit whose returned message could 
not be serialized, consistent with the DataWriter lifecycle documentation, and 
makes no claim that the SDK never aborts a committed writer.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to