github-actions[bot] commented on code in PR #62689:
URL: https://github.com/apache/doris/pull/62689#discussion_r3198506451
##########
be/src/service/CMakeLists.txt:
##########
@@ -43,6 +43,7 @@ if (${MAKE_TEST} STREQUAL "OFF" AND ${BUILD_BENCHMARK}
STREQUAL "OFF")
target_link_libraries(doris_be
${DORIS_LINK_LIBS}
Review Comment:
This links `paimon_rawlog_compat` even when `ENABLE_PAIMON_CPP` is off,
while `be/CMakeLists.txt` already adds the same library to `DORIS_LINK_LIBS`
when Paimon C++ is enabled. The compat object defines `google::RawLog__`, so
linking it unconditionally bypasses the intended feature gate and can create
duplicate/incorrect symbol resolution depending on whether glog or paimon-cpp
also provides that symbol. Please keep this behind the same gate and avoid
adding it twice.
##########
be/src/vec/sink/writer/paimon/paimon_doris_hdfs_file_system.cpp:
##########
@@ -0,0 +1,439 @@
+// 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 "vec/sink/writer/paimon/paimon_doris_hdfs_file_system.h"
+
+#ifdef WITH_PAIMON_CPP
+
+#include <gen_cpp/PlanNodes_types.h>
+
+#include <algorithm>
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "common/status.h"
+#include "io/fs/file_reader.h"
+#include "io/fs/file_system.h"
+#include "io/fs/file_writer.h"
+#include "io/fs/hdfs_file_system.h"
+#include "io/fs/path.h"
+#include "io/hdfs_builder.h"
+#include "paimon/factories/factory_creator.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/fs/file_system_factory.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "util/slice.h"
+
+namespace {
+
+std::string _extract_hdfs_fs_name(const std::string& uri) {
+ auto starts_with = [&](const char* prefix) { return uri.rfind(prefix, 0)
== 0; };
+ std::string_view scheme;
+ if (starts_with("hdfs://")) {
+ scheme = "hdfs://";
+ } else if (starts_with("dfs://")) {
+ scheme = "dfs://";
+ } else {
+ return {};
+ }
+ size_t authority_start = scheme.size();
+ size_t first_slash = uri.find('/', authority_start);
+ if (first_slash == std::string::npos) {
+ return uri;
+ }
+ if (first_slash == authority_start) {
+ return {};
+ }
+ return uri.substr(0, first_slash);
+}
+
+paimon::Status _to_paimon_status(const doris::Status& st) {
+ if (st.ok()) {
+ return paimon::Status::OK();
+ }
+ return paimon::Status::IOError(st.to_string());
+}
+
+class DorisPaimonBasicFileStatus final : public paimon::BasicFileStatus {
+public:
+ DorisPaimonBasicFileStatus(std::string path, bool is_dir)
+ : _path(std::move(path)), _is_dir(is_dir) {}
+
+ bool IsDir() const override { return _is_dir; }
+
+ std::string GetPath() const override { return _path; }
+
+private:
+ std::string _path;
+ bool _is_dir;
+};
+
+class DorisPaimonFileStatus final : public paimon::FileStatus {
+public:
+ DorisPaimonFileStatus(std::string path, bool is_dir, uint64_t len, int64_t
mtime_ms)
+ : _path(std::move(path)), _is_dir(is_dir), _len(len),
_mtime_ms(mtime_ms) {}
+
+ uint64_t GetLen() const override { return _len; }
+
+ bool IsDir() const override { return _is_dir; }
+
+ std::string GetPath() const override { return _path; }
+
+ int64_t GetModificationTime() const override { return _mtime_ms; }
+
+private:
+ std::string _path;
+ bool _is_dir;
+ uint64_t _len;
+ int64_t _mtime_ms;
+};
+
+class DorisPaimonInputStream final : public paimon::InputStream {
+public:
+ DorisPaimonInputStream(doris::io::FileReaderSPtr reader, std::string uri)
+ : _reader(std::move(reader)), _uri(std::move(uri)) {}
+
+ paimon::Status Close() override { return
_to_paimon_status(_reader->close()); }
+
+ paimon::Status Seek(int64_t offset, paimon::SeekOrigin origin) override {
+ int64_t base = 0;
+ switch (origin) {
+ case paimon::FS_SEEK_SET:
+ base = 0;
+ break;
+ case paimon::FS_SEEK_CUR:
+ base = _pos;
+ break;
+ case paimon::FS_SEEK_END: {
+ auto len = Length();
+ if (!len.ok()) {
+ return len.status();
+ }
+ base = static_cast<int64_t>(len.value());
+ break;
+ }
+ default:
+ return paimon::Status::Invalid("invalid seek origin");
+ }
+ int64_t next = base + offset;
+ if (next < 0) {
+ return paimon::Status::Invalid("negative seek position: ", next);
+ }
+ _pos = next;
+ return paimon::Status::OK();
+ }
+
+ paimon::Result<int64_t> GetPos() const override { return _pos; }
+
+ paimon::Result<int32_t> Read(char* buffer, uint32_t size) override {
+ size_t bytes_read = 0;
+ doris::Slice slice(buffer, size);
+ doris::Status st = _reader->read_at(_pos, slice, &bytes_read, nullptr);
+ if (!st.ok()) {
+ return paimon::Status::IOError(st.to_string());
+ }
+ _pos += static_cast<int64_t>(bytes_read);
+ return static_cast<int32_t>(bytes_read);
+ }
+
+ paimon::Result<int32_t> Read(char* buffer, uint32_t size, uint64_t offset)
override {
+ size_t bytes_read = 0;
+ doris::Slice slice(buffer, size);
+ doris::Status st = _reader->read_at(offset, slice, &bytes_read,
nullptr);
+ if (!st.ok()) {
+ return paimon::Status::IOError(st.to_string());
+ }
+ return static_cast<int32_t>(bytes_read);
+ }
+
+ void ReadAsync(char* buffer, uint32_t size, uint64_t offset,
+ std::function<void(paimon::Status)>&& callback) override {
+ auto res = Read(buffer, size, offset);
+ if (res.ok()) {
+ callback(paimon::Status::OK());
+ } else {
+ callback(res.status());
+ }
+ }
+
+ paimon::Result<std::string> GetUri() const override { return _uri; }
+
+ paimon::Result<uint64_t> Length() const override {
+ return static_cast<uint64_t>(_reader->size());
+ }
+
+private:
+ doris::io::FileReaderSPtr _reader;
+ std::string _uri;
+ int64_t _pos = 0;
+};
+
+class DorisPaimonOutputStream final : public paimon::OutputStream {
+public:
+ DorisPaimonOutputStream(doris::io::FileWriterPtr writer, std::string uri)
+ : _writer(std::move(writer)), _uri(std::move(uri)) {}
+
+ paimon::Status Close() override { return
_to_paimon_status(_writer->close()); }
+
+ paimon::Result<int32_t> Write(const char* buffer, uint32_t size) override {
+ doris::Slice slice(buffer, size);
+ doris::Status st = _writer->append(slice);
+ if (!st.ok()) {
+ return paimon::Status::IOError(st.to_string());
+ }
+ _pos += size;
+ return static_cast<int32_t>(size);
+ }
+
+ paimon::Status Flush() override { return paimon::Status::OK(); }
+
+ paimon::Result<int64_t> GetPos() const override { return _pos; }
+
+ paimon::Result<std::string> GetUri() const override { return _uri; }
+
+private:
+ doris::io::FileWriterPtr _writer;
+ std::string _uri;
+ int64_t _pos = 0;
+};
+
+class DorisPaimonFileSystem final : public paimon::FileSystem {
+public:
+ explicit DorisPaimonFileSystem(std::shared_ptr<doris::io::FileSystem> fs)
+ : _fs(std::move(fs)) {}
+
+ paimon::Result<std::unique_ptr<paimon::InputStream>> Open(
+ const std::string& path) const override {
+ doris::io::FileReaderSPtr reader;
+ doris::Status st = _fs->open_file(doris::io::Path(path), &reader,
nullptr);
+ if (!st.ok()) {
+ return paimon::Status::IOError(st.to_string());
+ }
+ return std::make_unique<DorisPaimonInputStream>(std::move(reader),
path);
+ }
+
+ paimon::Result<std::unique_ptr<paimon::OutputStream>> Create(const
std::string& path,
+ bool
overwrite) const override {
+ bool exists = false;
+ doris::Status exists_st = _fs->exists(doris::io::Path(path), &exists);
+ if (!exists_st.ok()) {
+ return paimon::Status::IOError(exists_st.to_string());
+ }
+ if (exists) {
+ if (!overwrite) {
+ return paimon::Status::Exist("path already exists: ", path);
+ }
+ doris::Status del_st =
_fs->delete_directory(doris::io::Path(path));
Review Comment:
For `Create(path, overwrite=true)`, this deletes `path` as a directory
before trying to delete it as a file. On HDFS `delete_directory` maps to
recursive `hdfsDelete(..., 1)`, so if the target path is unexpectedly a
directory this will remove the whole subtree. Paimon create/overwrite is for
file paths; please delete files as files first and reject directories instead
of recursively deleting them.
##########
be/src/exec/CMakeLists.txt:
##########
@@ -23,6 +23,17 @@ set(EXECUTABLE_OUTPUT_PATH "${BUILD_DIR}/src/exec")
file(GLOB_RECURSE EXEC_FILES CONFIGURE_DEPENDS *.cpp)
+if (ENABLE_PAIMON_CPP)
Review Comment:
This gate makes `ENABLE_PAIMON_CPP=OFF` builds link-fail. `operator.cpp` and
`pipeline_fragment_context.cpp` now unconditionally include and instantiate
`PaimonTableSinkOperatorX` / `AsyncWriterSink<VPaimonTableWriter>`, but the
definitions for `PaimonTableSinkLocalState::{init,close}` and
`VPaimonTableWriter` are only added to `Exec` inside this block. Since
`ENABLE_PAIMON_CPP` remains an overridable build option, either the
unconditional references need to be guarded too, or the non-paimon-cpp stubs
need to be built so this configuration still links.
##########
be/src/vec/sink/writer/paimon/vpaimon_partition_writer.cpp:
##########
@@ -0,0 +1,269 @@
+// 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 "vec/sink/writer/paimon/vpaimon_partition_writer.h"
+
+#include <gen_cpp/DataSinks_types.h>
+
+#include <map>
+#include <utility>
+
+#include "common/metrics/doris_metrics.h"
+#include "format/arrow/arrow_block_convertor.h"
+#include "format/arrow/arrow_row_batch.h"
+#include "runtime/runtime_state.h"
+
+#ifdef WITH_PAIMON_CPP
+#include <arrow/array.h>
+#include <arrow/c/bridge.h>
+#include <arrow/memory_pool.h>
+#include <arrow/record_batch.h>
+#include <arrow/status.h>
+#include <arrow/type.h>
+
+#include <string>
+
+#include "paimon/file_store_write.h"
+#include "paimon/record_batch.h"
+#endif
+
+namespace doris {
+namespace vectorized {
+
+#ifdef WITH_PAIMON_CPP
+namespace {
+class PaimonArrowMemPoolAdaptor : public arrow::MemoryPool {
+public:
+ explicit PaimonArrowMemPoolAdaptor(std::shared_ptr<::paimon::MemoryPool>
pool)
+ : pool_(std::move(pool)) {}
+
+ arrow::Status Allocate(int64_t size, int64_t alignment, uint8_t** out)
override {
+ *out = reinterpret_cast<uint8_t*>(pool_->Malloc(size, alignment));
Review Comment:
`pool_->Malloc()` can return `nullptr` under memory pressure, but this Arrow
`MemoryPool` reports `Status::OK()` and even records the allocation. Arrow
callers treat OK as a valid non-null buffer and can then dereference a null
pointer or corrupt memory. The adapter should return an out-of-memory
`arrow::Status` when `Malloc`/`Realloc` returns null and only update stats
after a successful allocation.
##########
be/src/vec/sink/vpaimon_table_writer.cpp:
##########
@@ -0,0 +1,740 @@
+// 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 "vec/sink/vpaimon_table_writer.h"
+
+#include <gen_cpp/DataSinks_types.h>
+
+#include <algorithm>
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "common/metrics/doris_metrics.h"
+#include "core/block/block.h"
+#include "core/column/column.h"
+#include "exprs/vexpr.h"
+#include "exprs/vexpr_context.h"
+#include "runtime/query_context.h"
+#include "runtime/runtime_profile.h"
+#include "runtime/runtime_state.h"
+#include "util/defer_op.h"
+#include "vec/sink/paimon_writer_utils.h"
+#include "vec/sink/writer/paimon/paimon_doris_hdfs_file_system.h"
+#include "vec/sink/writer/paimon/vpaimon_partition_writer.h"
+
+#ifdef WITH_PAIMON_CPP
+#include <arrow/array.h>
+#include <arrow/c/bridge.h>
+#include <arrow/record_batch.h>
+#include <arrow/type.h>
+
+#include <cstdint>
+
+#include "format/arrow/arrow_block_convertor.h"
+#include "format/arrow/arrow_row_batch.h"
+#include "format/parquet/arrow_memory_pool.h"
+#include "io/fs/hdfs_file_system.h"
+#include "io/hdfs_builder.h"
+#include "paimon/commit_message.h"
+#include "paimon/factories/factory_creator.h"
+#include "paimon/file_store_write.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/metrics.h"
+#include "paimon/utils/bucket_id_calculator.h"
+#include "paimon/write_context.h"
+#include "vec/sink/writer/paimon/paimon_doris_memory_pool.h"
+
+// Force link paimon file format factories
+namespace paimon {
+namespace parquet {}
+} // namespace paimon
+
+#endif
+
+namespace doris {
+namespace vectorized {
+
+#ifdef WITH_PAIMON_CPP
+namespace {
+bool is_paimon_cpp_time_metric(std::string_view name) {
+ return name.size() > 3 && name.substr(name.size() - 3) == "_ns";
+}
+
+void attach_paimon_cpp_metrics_to_profile(RuntimeProfile* profile,
+ const
std::shared_ptr<::paimon::Metrics>& metrics) {
+ if (profile == nullptr || !metrics) {
+ return;
+ }
+ auto all = metrics->GetAllCounters();
+ if (all.empty()) {
+ return;
+ }
+ for (const auto& kv : all) {
+ std::string counter_name = "PaimonCpp_" + kv.first;
+ std::replace(counter_name.begin(), counter_name.end(), '.', '_');
+ RuntimeProfile::Counter* counter = nullptr;
+ if (is_paimon_cpp_time_metric(kv.first)) {
+ counter = ADD_COUNTER(profile, counter_name, TUnit::TIME_NS);
+ } else {
+ counter = ADD_COUNTER(profile, counter_name, TUnit::UNIT);
+ }
+ COUNTER_UPDATE(counter, kv.second);
+ }
+}
+} // namespace
+#endif
+
+VPaimonTableWriter::~VPaimonTableWriter() = default;
+
+VPaimonTableWriter::VPaimonTableWriter(const TDataSink& t_sink,
+ const VExprContextSPtrs& output_exprs)
+ : VPaimonTableWriter(t_sink, output_exprs, nullptr, nullptr) {}
+
+VPaimonTableWriter::VPaimonTableWriter(const TDataSink& t_sink,
+ const VExprContextSPtrs& output_exprs,
+ std::shared_ptr<Dependency> dep,
+ std::shared_ptr<Dependency> fin_dep)
+ : AsyncResultWriter(output_exprs, std::move(dep), std::move(fin_dep)),
_t_sink(t_sink) {
+ DCHECK(_t_sink.__isset.paimon_table_sink);
+}
+
+Status VPaimonTableWriter::init_properties(ObjectPool* /*pool*/) {
+ // Currently there is no extra property to initialize. Kept for symmetry
+ // with VIcebergTableWriter and future paimon-cpp wiring.
+ return Status::OK();
+}
+
+Status VPaimonTableWriter::open(RuntimeState* state, RuntimeProfile* profile) {
+ _state = state;
+ _profile = profile;
+#ifndef WITH_PAIMON_CPP
+ return Status::NotSupported("paimon-cpp is not enabled");
+#else
+ _written_rows_counter = ADD_COUNTER(_profile, "WrittenRows", TUnit::UNIT);
+ _written_bytes_counter = ADD_COUNTER(_profile, "WrittenBytes",
TUnit::BYTES);
+ _send_data_timer = ADD_TIMER(_profile, "SendDataTime");
+ _project_timer = ADD_CHILD_TIMER(_profile, "ProjectTime", "SendDataTime");
+ _bucket_calc_timer = ADD_CHILD_TIMER(_profile, "BucketCalcTime",
"SendDataTime");
+ _partition_writers_dispatch_timer =
+ ADD_CHILD_TIMER(_profile, "PartitionsDispatchTime",
"SendDataTime");
+ _partition_writers_write_timer =
+ ADD_CHILD_TIMER(_profile, "PartitionsWriteTime", "SendDataTime");
+ _partition_writers_count = ADD_COUNTER(_profile, "PartitionsWriteCount",
TUnit::UNIT);
+ _partition_writer_created = ADD_COUNTER(_profile,
"PartitionWriterCreated", TUnit::UNIT);
+ _open_timer = ADD_TIMER(_profile, "OpenTime");
+ _close_timer = ADD_TIMER(_profile, "CloseTime");
+ _prepare_commit_timer = ADD_TIMER(_profile, "PrepareCommitTime");
+ _serialize_commit_messages_timer = ADD_TIMER(_profile,
"SerializeCommitMessagesTime");
+ _commit_payload_bytes_counter = ADD_COUNTER(_profile,
"CommitPayloadBytes", TUnit::BYTES);
+
+ SCOPED_TIMER(_open_timer);
+
+ ensure_paimon_doris_hdfs_file_system_registered();
+
+ auto registered_types =
paimon::FactoryCreator::GetInstance()->GetRegisteredType();
+ std::string types_str;
+ bool has_parquet = false;
+ for (const auto& t : registered_types) {
+ types_str += t + ", ";
+ has_parquet |= (t == "parquet");
+ }
+ if (!has_parquet) {
+ return Status::InternalError(
+ "paimon-cpp parquet file format factory is not registered
(missing 'parquet' in "
+ "FactoryCreator). Please ensure BE is built with
WITH_PAIMON_CPP=ON and linked "
+ "with libpaimon_parquet_file_format.a (whole-archive).
Registered factories: {}",
+ types_str);
+ }
+
+ _pool =
std::make_shared<PaimonDorisMemoryPool>(_state->query_mem_tracker());
+ const auto& paimon_sink = _t_sink.paimon_table_sink;
+ if (!paimon_sink.__isset.table_location ||
paimon_sink.table_location.empty()) {
+ return Status::InvalidArgument("paimon table location is empty");
+ }
+ std::string commit_user;
+ if (paimon_sink.__isset.options) {
+ auto it = paimon_sink.options.find("doris.commit_user");
+ if (it != paimon_sink.options.end()) {
+ commit_user = it->second;
+ }
+ }
+ if (commit_user.empty()) {
+ commit_user = _state->user();
+ }
+
+ std::map<std::string, std::string> options;
+ if (paimon_sink.__isset.options) {
+ for (const auto& kv : paimon_sink.options) {
+ if (kv.first.rfind("doris.", 0) == 0) {
+ continue;
+ }
+ options.emplace(kv.first, kv.second);
+ }
+ }
+
+ // Workaround for paimon-cpp issue where it defaults to LocalFileSystem if
path has no scheme.
+ // If table_location is missing scheme (common in HDFS setup without full
URI),
+ // and fs.defaultFS is provided in options, we prepend it.
+ std::string table_location = paimon_sink.table_location;
+ if (table_location.find("://") == std::string::npos) {
+ auto it = options.find("fs.defaultFS");
+ if (it != options.end()) {
+ std::string default_fs = it->second;
+ // Remove trailing slash from default_fs if present
+ while (!default_fs.empty() && default_fs.back() == '/') {
+ default_fs.pop_back();
+ }
+ // Remove leading slash from table_location if present
+ if (!table_location.empty() && table_location.front() == '/') {
+ table_location = default_fs + table_location;
+ } else {
+ table_location = default_fs + "/" + table_location;
+ }
+ }
+ }
+
+ int64_t buffer_size = 256 * 1024 * 1024L; // Default 256MB
+
+ if (_state->query_options().__isset.paimon_write_buffer_size &&
+ _state->query_options().paimon_write_buffer_size > 0) {
+ buffer_size = _state->query_options().paimon_write_buffer_size;
+ }
+
+ bool enable_adaptive = true;
+ if (_state->query_options().__isset.enable_paimon_adaptive_buffer_size) {
+ enable_adaptive =
_state->query_options().enable_paimon_adaptive_buffer_size;
+ }
+
+ if (enable_adaptive && paimon_sink.__isset.bucket_num &&
paimon_sink.bucket_num > 0) {
+ int bucket_num = paimon_sink.bucket_num;
+ buffer_size = get_paimon_write_buffer_size(buffer_size, true,
bucket_num);
+ LOG(INFO) << "Adaptive Paimon Buffer Size: bucket_num=" << bucket_num
+ << ", adjusted_buffer_size=" << buffer_size;
+ }
+ LOG(INFO) << "Paimon Native Writer Final Buffer Size: " << buffer_size
+ << " (enable_adaptive=" << enable_adaptive << ")";
+ options["write-buffer-size"] = std::to_string(buffer_size);
+
+ if (_state->query_options().__isset.paimon_target_file_size &&
+ _state->query_options().paimon_target_file_size > 0) {
+ options["target-file-size"] =
+
std::to_string(_state->query_options().paimon_target_file_size);
+ LOG(INFO) << "Paimon Native Writer Target File Size: "
+ << _state->query_options().paimon_target_file_size;
+ }
+
+ options["file.format"] = "parquet";
Review Comment:
The native writer unconditionally overrides the table options to parquet.
For an existing Paimon table configured with ORC (which FE currently advertises
as supported), this either writes data/manifest files in the wrong format or
fails only after BE starts execution. The writer should honor the table's
configured format, or FE should reject ORC for the native path and require the
JNI writer before planning the sink.
--
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]