diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index 587edfc52..766d34b2e 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -60,7 +60,9 @@ foreach(example scheduled_send service_bus multithreaded_client - multithreaded_client_flow_control) + multithreaded_client_flow_control + tx_send + tx_recv) add_executable(${example} ${example}.cpp) target_link_libraries(${example} Proton::cpp Threads::Threads) endforeach() diff --git a/cpp/examples/simple_recv.cpp b/cpp/examples/simple_recv.cpp index 0dadf75b8..fece83958 100644 --- a/cpp/examples/simple_recv.cpp +++ b/cpp/examples/simple_recv.cpp @@ -43,10 +43,11 @@ class simple_recv : public proton::messaging_handler { proton::receiver receiver; int expected; int received; + bool verbose; public: - simple_recv(const std::string &s, const std::string &u, const std::string &p, int c) : - url(s), user(u), password(p), expected(c), received(0) {} + simple_recv(const std::string &s, const std::string &u, const std::string &p, int c, bool verbose) : + url(s), user(u), password(p), expected(c), received(0), verbose(verbose) {} void on_container_start(proton::container &c) override { proton::connection_options co; @@ -61,6 +62,9 @@ class simple_recv : public proton::messaging_handler { } if (expected == 0 || received < expected) { + if (verbose) { + std::cout << msg << ": "; + } std::cout << msg.body() << std::endl; received++; @@ -77,18 +81,20 @@ int main(int argc, char **argv) { std::string user; std::string password; int message_count = 100; + bool verbose; example::options opts(argc, argv); opts.add_value(address, 'a', "address", "connect to and receive from URL", "URL"); opts.add_value(message_count, 'm', "messages", "receive COUNT messages", "COUNT"); opts.add_value(user, 'u', "user", "authenticate as USER", "USER"); opts.add_value(password, 'p', "password", "authenticate with PASSWORD", "PASSWORD"); + opts.add_flag(verbose, 'v', "verbose", "show whole message contents"); try { opts.parse(); - simple_recv recv(address, user, password, message_count); + simple_recv recv(address, user, password, message_count, verbose); proton::container(recv).run(); return 0; diff --git a/cpp/examples/tx_recv.cpp b/cpp/examples/tx_recv.cpp new file mode 100644 index 000000000..df550dd82 --- /dev/null +++ b/cpp/examples/tx_recv.cpp @@ -0,0 +1,151 @@ +/* + * + * 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 "options.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +class tx_recv : public proton::messaging_handler { + private: + proton::receiver receiver; + proton::session session; + std::string conn_url_; + std::string addr_; + int total; + int batch_size; + int received = 0; + int current_batch = 0; + int batch_index = 0; + + public: + tx_recv(const std::string& u, const std::string &a, int c, int b): + conn_url_(u), addr_(a), total(c), batch_size(b) {} + + void on_container_start(proton::container &c) override { + c.connect(conn_url_); + } + + void on_connection_open(proton::connection& c) override { + // NOTE:credit_window(0) disables automatic flow control. + // We will use flow control to receive batches of messages in a transaction. + std::cout << "In this example we abort/commit transaction alternatively." << std::endl; + receiver = c.open_receiver(addr_, proton::receiver_options().credit_window(0)); + } + + void on_session_open(proton::session &s) override { + if(!s.transaction_is_declared()) { + std::cout << "New session is open" << std::endl; + s.transaction_declare(*this); + session = s; + } else { + std::cout << "Transaction is declared: " << s.transaction_id() << std::endl; + receiver.add_credit(batch_size); + } + } + + void on_session_error(proton::session &s) override { + std::cout << "Session error: " << s.error().what() << std::endl; + s.connection().close(); + exit(-1); + } + + void on_session_transaction_committed(proton::session &s) override { + std::cout << "Transaction commited" << std::endl; + received += current_batch; + current_batch = 0; + if (received == total) { + std::cout << "All received messages committed, closing connection." << std::endl; + s.connection().close(); + } + else { + std::cout << "Re-declaring transaction now... to receive next batch." << std::endl; + s.transaction_declare(*this); + } + } + + void on_session_transaction_aborted(proton::session &s) override { + std::cout << "Transaction aborted!" << std::endl; + std::cout << "Re-delaring transaction now..." << std::endl; + current_batch = 0; + s.transaction_declare(*this); + } + + void on_message(proton::delivery &d, proton::message &msg) override { + std::cout<<"# MESSAGE: " << msg.id() <<": " << msg.body() << std::endl; + d.accept(); + current_batch += 1; + if (current_batch == batch_size) { + // Batch complete + if (batch_index % 2 == 1) { + std::cout << "Commiting transaction..." << std::endl; + session.transaction_commit(); + } else { + std::cout << "Aborting transaction..." << std::endl; + session.transaction_abort(); + } + batch_index++; + } + } +}; + +int main(int argc, char **argv) { + std::string conn_url = argc > 1 ? argv[1] : "//127.0.0.1:5672"; + std::string addr = argc > 2 ? argv[2] : "examples"; + int message_count = 6; + int batch_size = 3; + example::options opts(argc, argv); + + opts.add_value(conn_url, 'u', "url", "connect and send to URL", "URL"); + opts.add_value(addr, 'a', "address", "connect and send to address", "URL"); + opts.add_value(message_count, 'm', "messages", "number of messages to send", "COUNT"); + opts.add_value(batch_size, 'b', "batch_size", "number of messages in each transaction", "BATCH_SIZE"); + + try { + opts.parse(); + + tx_recv recv(conn_url, addr, message_count, batch_size); + proton::container(recv).run(); + + return 0; + } catch (const example::bad_option& e) { + std::cout << opts << std::endl << e.what() << std::endl; + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + } + + return 1; +} diff --git a/cpp/examples/tx_send.cpp b/cpp/examples/tx_send.cpp new file mode 100644 index 000000000..0a268c80c --- /dev/null +++ b/cpp/examples/tx_send.cpp @@ -0,0 +1,172 @@ +/* + * + * 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 "options.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +class tx_send : public proton::messaging_handler { + private: + proton::sender sender; + std::string conn_url_; + std::string addr_; + int total; + int batch_size; + int sent; + int batch_index = 0; + int current_batch = 0; + int committed = 0; + std::atomic unique_msg_id; + + public: + tx_send(const std::string& u, const std::string& a, int c, int b): + conn_url_(u), addr_(a), total(c), batch_size(b), sent(0), unique_msg_id(10000) {} + + void on_container_start(proton::container &c) override { + c.connect(conn_url_); + } + + void on_connection_open(proton::connection& c) override { + std::cout << "In this example we abort/commit transaction alternatively." << std::endl; + sender = c.open_sender(addr_); + } + + void on_session_open(proton::session& s) override { + if(!s.transaction_is_declared()) { + std::cout << "New session is open, declaring transaction now..." << std::endl; + s.transaction_declare(*this); + } else { + std::cout << "Transaction is declared: " << s.transaction_id() << std::endl; + send(); + } + } + + void on_session_error(proton::session &s) override { + std::cout << "Session error: " << s.error().what() << std::endl; + s.connection().close(); + exit(-1); + } + + void on_session_transaction_commit_failed(proton::session &s) override { + std::cout << "Transaction commit failed!" << std::endl; + s.connection().close(); + exit(-1); + } + + void on_sendable(proton::sender&) override { + send(); + } + + void send() { + proton::session session = sender.session(); + while (session.transaction_is_declared() && sender.credit() && + (committed + current_batch) < total) { + proton::message msg; + std::map m; + m["sequence"] = committed + current_batch; + + msg.id(std::atomic_fetch_add(&unique_msg_id, 1)); + msg.body(m); + std::cout << "Sending [sender batch " << batch_index << "]: " << msg << std::endl; + sender.send(msg); + current_batch += 1; + if(current_batch == batch_size) + { + if (batch_index % 2 == 0) { + std::cout << "Commiting transaction..." << std::endl; + session.transaction_commit(); + } else { + std::cout << "Aborting transaction..." << std::endl; + session.transaction_abort(); + } + batch_index++; + } + } + } + + void on_session_transaction_committed(proton::session &s) override { + committed += current_batch; + current_batch = 0; + std::cout << "Transaction commited" << std::endl; + if(committed == total) { + std::cout << "All messages committed, closing connection." << std::endl; + s.connection().close(); + } + else { + std::cout << "Re-declaring transaction now..." << std::endl; + s.transaction_declare(*this); + } + } + + void on_session_transaction_aborted(proton::session &s) override { + std::cout << "Transaction aborted!" << std::endl; + std::cout << "Re-delaring transaction now..." << std::endl; + current_batch = 0; + s.transaction_declare(*this); + } + + void on_sender_close(proton::sender &s) override { + current_batch = 0; + } + +}; + +int main(int argc, char **argv) { + std::string conn_url = argc > 1 ? argv[1] : "//127.0.0.1:5672"; + std::string addr = argc > 2 ? argv[2] : "examples"; + int message_count = 6; + int batch_size = 3; + example::options opts(argc, argv); + + opts.add_value(conn_url, 'u', "url", "connect and send to URL", "URL"); + opts.add_value(addr, 'a', "address", "connect and send to address", "URL"); + opts.add_value(message_count, 'm', "messages", "number of messages to send", "COUNT"); + opts.add_value(batch_size, 'b', "batch_size", "number of messages in each transaction", "BATCH_SIZE"); + + try { + opts.parse(); + + tx_send send(conn_url, addr, message_count, batch_size); + proton::container(send).run(); + + return 0; + } catch (const example::bad_option& e) { + std::cout << opts << std::endl << e.what() << std::endl; + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + } + + return 1; +} diff --git a/cpp/include/proton/messaging_handler.hpp b/cpp/include/proton/messaging_handler.hpp index 213dbe73e..f41b99053 100644 --- a/cpp/include/proton/messaging_handler.hpp +++ b/cpp/include/proton/messaging_handler.hpp @@ -172,6 +172,15 @@ PN_CPP_CLASS_EXTERN messaging_handler { /// The remote peer closed the session with an error condition. PN_CPP_EXTERN virtual void on_session_error(session&); + /// Called when a local transaction is discharged successfully. + PN_CPP_EXTERN virtual void on_session_transaction_committed(session&); + + /// Called when the commit of a local transaction fails. + PN_CPP_EXTERN virtual void on_session_transaction_commit_failed(session&); + + /// Called when a local transaction is discharged unsuccessfully (aborted). + PN_CPP_EXTERN virtual void on_session_transaction_aborted(session&); + /// The remote peer opened the link. PN_CPP_EXTERN virtual void on_receiver_open(receiver&); diff --git a/cpp/include/proton/session.hpp b/cpp/include/proton/session.hpp index 60522c817..350c9dfa0 100644 --- a/cpp/include/proton/session.hpp +++ b/cpp/include/proton/session.hpp @@ -105,14 +105,22 @@ PN_CPP_CLASS_EXTERN session : public internal::object, public endp /// Get user data from this session. PN_CPP_EXTERN void* user_data() const; + PN_CPP_EXTERN void transaction_declare(proton::messaging_handler &handler, bool settle_before_discharge = false); + PN_CPP_EXTERN bool transaction_is_declared(); + PN_CPP_EXTERN proton::binary transaction_id() const; + PN_CPP_EXTERN void transaction_commit(); + PN_CPP_EXTERN void transaction_abort(); + + + /// @cond INTERNAL - friend class internal::factory; - friend class session_iterator; + friend class internal::factory; + friend class sender; + friend class session_iterator; /// @endcond }; /// @cond INTERNAL - /// An iterator of sessions. class session_iterator : public internal::iter_base { public: @@ -126,7 +134,6 @@ class session_iterator : public internal::iter_base { typedef internal::iter_range session_range; /// @endcond - } // proton #endif // PROTON_SESSION_HPP diff --git a/cpp/include/proton/target_options.hpp b/cpp/include/proton/target_options.hpp index f5fe99177..b612671ab 100644 --- a/cpp/include/proton/target_options.hpp +++ b/cpp/include/proton/target_options.hpp @@ -60,6 +60,10 @@ class target_options { /// address is ignored if dynamic() is true. PN_CPP_EXTERN target_options& address(const std::string& addr); + /// Set the target be of type coordinator. + /// This immediately override the currently assigned type. + PN_CPP_EXTERN target_options& make_coordinator(); + /// Request that a node be dynamically created by the remote peer. /// The default is false. Any specified target address() is /// ignored if true. diff --git a/cpp/include/proton/transaction_handler.hpp b/cpp/include/proton/transaction_handler.hpp new file mode 100644 index 000000000..1e229fbf5 --- /dev/null +++ b/cpp/include/proton/transaction_handler.hpp @@ -0,0 +1,61 @@ +#ifndef PROTON_TRANSACTION_HPP +#define PROTON_TRANSACTION_HPP + + +/* + * + * 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 "./fwd.hpp" +#include "./internal/export.hpp" +#include "./sender.hpp" +#include "./tracker.hpp" +#include "./container.hpp" + +/// @file +/// @copybrief proton::transaction + +namespace proton { + +class +PN_CPP_CLASS_EXTERN transaction_handler { + public: + PN_CPP_EXTERN virtual ~transaction_handler(); + + /// Called when a local transaction is declared. + PN_CPP_EXTERN virtual void on_transaction_declared(session); + + /// Called when a local transaction is discharged successfully. + PN_CPP_EXTERN virtual void on_transaction_committed(session); + + /// Called when a local transaction is discharged unsuccessfully (aborted). + PN_CPP_EXTERN virtual void on_transaction_aborted(session); + + /// Called when a local transaction declare fails. + PN_CPP_EXTERN virtual void on_transaction_declare_failed(session); + + /// Called when the commit of a local transaction fails. + PN_CPP_EXTERN virtual void on_transaction_commit_failed(session); +}; + +} // namespace proton + +#endif // PROTON_TRANSACTION_HPP diff --git a/cpp/src/contexts.hpp b/cpp/src/contexts.hpp index 7ebab1d7b..5f0bdc923 100644 --- a/cpp/src/contexts.hpp +++ b/cpp/src/contexts.hpp @@ -25,6 +25,7 @@ #include "reconnect_options_impl.hpp" #include "proton/work_queue.hpp" +#include "proton/session.hpp" #include "proton/message.hpp" #include "proton/object.h" @@ -41,6 +42,7 @@ namespace proton { class proton_handler; class connector; +class transaction_impl; namespace io {class link_namer;} @@ -152,6 +154,7 @@ class session_context : public context { session_context() : handler(0), user_data_(nullptr) {} static session_context& get(pn_session_t* s); + transaction_impl* _txn_impl; messaging_handler* handler; void* user_data_; }; diff --git a/cpp/src/delivery.cpp b/cpp/src/delivery.cpp index 8dc050e25..7f3633f0e 100644 --- a/cpp/src/delivery.cpp +++ b/cpp/src/delivery.cpp @@ -34,7 +34,15 @@ namespace { void settle_delivery(pn_delivery_t* o, uint64_t state) { - pn_delivery_update(o, state); + proton::session session = proton::make_wrapper(o).session(); + if(session.transaction_is_declared()) { + // Transactional disposition + auto disp = pn_transactional_disposition(pn_delivery_local(o)); + pn_transactional_disposition_set_id(disp, pn_bytes(session.transaction_id())); + pn_transactional_disposition_set_outcome_type(disp, state); + } else { + pn_delivery_update(o, state); + } pn_delivery_settle(o); } diff --git a/cpp/src/handler.cpp b/cpp/src/handler.cpp index 1632efda6..c62966cd9 100644 --- a/cpp/src/handler.cpp +++ b/cpp/src/handler.cpp @@ -65,6 +65,10 @@ void messaging_handler::on_session_open(session &s) { pn_session_open(unwrap(s)); } } +void messaging_handler::on_session_transaction_committed(session &) {} +void messaging_handler::on_session_transaction_commit_failed(session &) {} +void messaging_handler::on_session_transaction_aborted(session &) {} + void messaging_handler::on_receiver_close(receiver &) {} void messaging_handler::on_receiver_error(receiver &l) { on_error(l.error()); } void messaging_handler::on_receiver_open(receiver &l) { diff --git a/cpp/src/messaging_adapter.cpp b/cpp/src/messaging_adapter.cpp index f90cd7613..b68f49fe2 100644 --- a/cpp/src/messaging_adapter.cpp +++ b/cpp/src/messaging_adapter.cpp @@ -30,6 +30,7 @@ #include "proton/receiver_options.hpp" #include "proton/sender.hpp" #include "proton/sender_options.hpp" +#include "proton/target_options.hpp" #include "proton/session.hpp" #include "proton/tracker.hpp" #include "proton/transport.hpp" @@ -40,6 +41,7 @@ #include #include +#include #include #include #include @@ -69,7 +71,8 @@ void on_link_flow(messaging_handler& handler, pn_event_t* event) { // TODO: process session flow data, if no link-specific data, just return. if (!lnk) return; int state = pn_link_state(lnk); - if ((state&PN_LOCAL_ACTIVE) && (state&PN_REMOTE_ACTIVE)) { + if (pn_terminus_get_type(pn_link_remote_target(lnk)) == PN_COORDINATOR || + ((state & PN_LOCAL_ACTIVE) && (state & PN_REMOTE_ACTIVE))) { link_context& lctx = link_context::get(lnk); if (pn_link_is_sender(lnk)) { if (pn_link_credit(lnk) > 0) { @@ -274,13 +277,6 @@ void on_link_local_open(messaging_handler& handler, pn_event_t* event) { void on_link_remote_open(messaging_handler& handler, pn_event_t* event) { auto lnk = pn_event_link(event); - // Currently don't implement (transaction) coordinator - if (pn_terminus_get_type(pn_link_remote_target(lnk))==PN_COORDINATOR) { - auto error = pn_link_condition(lnk); - pn_condition_set_name(error, "amqp:not-implemented"); - pn_link_close(lnk); - return; - } if (pn_link_state(lnk) & PN_LOCAL_UNINIT) { // Incoming link // Copy source and target from remote end. pn_terminus_copy(pn_link_source(lnk), pn_link_remote_source(lnk)); diff --git a/cpp/src/node_options.cpp b/cpp/src/node_options.cpp index fd489baf3..2c172e4a1 100644 --- a/cpp/src/node_options.cpp +++ b/cpp/src/node_options.cpp @@ -162,6 +162,7 @@ class target_options::impl { option expiry_policy; option > capabilities; option dynamic_properties; + option is_coordinator; void apply(target& t) { node_address(t, address, dynamic, anonymous); @@ -175,6 +176,9 @@ class target_options::impl { get(dynamic_properties.value, target_map); value(pn_terminus_properties(unwrap(t))) = target_map; } + if (is_coordinator.set && is_coordinator.value) { + pn_terminus_set_type(unwrap(t), pn_terminus_type_t(PN_COORDINATOR)); + } } }; @@ -201,8 +205,8 @@ target_options& target_options::dynamic_properties(const target::dynamic_propert return *this; } -void target_options::apply(target& s) const { impl_->apply(s); } - +target_options& target_options::make_coordinator() { impl_->is_coordinator = true; return *this; } +void target_options::apply(target& s) const { impl_->apply(s); } } // namespace proton diff --git a/cpp/src/sender.cpp b/cpp/src/sender.cpp index 942e755b0..4a6bcd148 100644 --- a/cpp/src/sender.cpp +++ b/cpp/src/sender.cpp @@ -26,10 +26,12 @@ #include "proton/source.hpp" #include "proton/target.hpp" #include "proton/tracker.hpp" +#include "types_internal.hpp" #include #include #include +#include #include "proton_bits.hpp" #include "contexts.hpp" @@ -84,6 +86,13 @@ tracker sender::send(const message &message, const binary &tag) { pn_delivery_settle(dlv); if (!pn_link_credit(pn_object())) link_context::get(pn_object()).draining = false; + + // If transaction is declared + if (session().transaction_is_declared()) { + auto disp = pn_transactional_disposition(pn_delivery_local(unwrap(track))); + pn_transactional_disposition_set_id(disp, pn_bytes(session().transaction_id())); + } + return track; } diff --git a/cpp/src/session.cpp b/cpp/src/session.cpp index b8f777a00..13d43229c 100644 --- a/cpp/src/session.cpp +++ b/cpp/src/session.cpp @@ -21,13 +21,22 @@ #include "proton/session.hpp" #include "proton/connection.hpp" +#include "proton/delivery.h" +#include "proton/delivery.hpp" +#include "proton/error.hpp" #include "proton/receiver_options.hpp" #include "proton/sender_options.hpp" #include "proton/session_options.hpp" +#include "proton/target_options.hpp" +#include "proton/messaging_handler.hpp" +#include "proton/tracker.hpp" +#include "proton/transfer.hpp" +#include "types_internal.hpp" #include "contexts.hpp" #include "link_namer.hpp" #include "proton_bits.hpp" +#include #include #include @@ -70,6 +79,7 @@ std::string next_link_name(const connection& c) { return ln ? ln->link_name() : uuid::random().str(); } + } sender session::open_sender(const std::string &addr) { @@ -148,4 +158,214 @@ void* session::user_data() const { return sctx.user_data_; } +class transaction_impl { + public: + proton::sender txn_ctrl; + proton::messaging_handler *handler = nullptr; + proton::binary transaction_id; + bool failed = false; + enum State { + FREE, + DECLARING, + DECLARED, + DISCHARGING, + }; + enum State state = State::FREE; + std::vector pending; + + void commit(); + void abort(); + void declare(); + + void discharge(bool failed); + void release_pending(); + + proton::tracker send_ctrl(proton::symbol descriptor, proton::value _value); + void handle_outcome(proton::tracker t); + transaction_impl(proton::sender &_txn_ctrl, + proton::messaging_handler &_handler, + bool _settle_before_discharge); + ~transaction_impl(); +}; + + +namespace { + +bool transaction_is_empty(const session& s) { return session_context::get(unwrap(s))._txn_impl == NULL; } + +void transaction_handle_outcome(const session& s, proton::tracker t) { + session_context::get(unwrap(s))._txn_impl->handle_outcome(t); +} + +void transaction_delete(const session& s) { auto &_txn_impl = session_context::get(unwrap(s))._txn_impl; delete _txn_impl; _txn_impl = nullptr;} + +} + +void session::transaction_declare(proton::messaging_handler &handler, bool settle_before_discharge) { + auto &txn_impl = session_context::get(pn_object())._txn_impl; + if (txn_impl == nullptr) { + // Create _txn_impl + proton::connection conn = this->connection(); + class InternalTransactionHandler : public proton::messaging_handler { + + void on_tracker_settle(proton::tracker &t) override { + if (!transaction_is_empty(t.session())) { + transaction_handle_outcome(t.session(), t); + } + } + }; + + proton::target_options opts; + std::vector cap = {proton::symbol("amqp:local-transactions")}; + opts.capabilities(cap); + opts.make_coordinator(); + + proton::sender_options so; + so.name("txn-ctrl"); + so.target(opts); + + static InternalTransactionHandler internal_handler; // internal_handler going out of scope. Fix it + so.handler(internal_handler); + + static proton::sender s = conn.open_sender("does not matter", so); + + settle_before_discharge = false; + + txn_impl = new transaction_impl(s, handler, settle_before_discharge); + } + // Declare txn + txn_impl->declare(); +} + + +proton::binary session::transaction_id() const { return session_context::get(pn_object())._txn_impl->transaction_id; } +void session::transaction_commit() { session_context::get(pn_object())._txn_impl->commit(); } +void session::transaction_abort() { session_context::get(pn_object())._txn_impl->abort(); } +bool session::transaction_is_declared() { return (!transaction_is_empty(*this)) && session_context::get(pn_object())._txn_impl->state == transaction_impl::State::DECLARED; } + +transaction_impl::transaction_impl(proton::sender &_txn_ctrl, + proton::messaging_handler &_handler, + bool _settle_before_discharge) + : txn_ctrl(_txn_ctrl), handler(&_handler) { +} +transaction_impl::~transaction_impl() {} + +void transaction_impl::commit() { + discharge(false); +} + +void transaction_impl::abort() { + discharge(true); +} + +void transaction_impl::declare() { + if (state != transaction_impl::State::FREE) + throw proton::error("This session has some associcated transaction already"); + state = State::DECLARING; + + proton::symbol descriptor("amqp:declare:list"); + std::list vd; + proton::value i_am_null; + vd.push_back(i_am_null); + proton::value _value = vd; + send_ctrl(descriptor, _value); +} + +void transaction_impl::discharge(bool _failed) { + if (state != transaction_impl::State::DECLARED) + throw proton::error("Only a declared txn can be discharged."); + state = State::DISCHARGING; + + failed = _failed; + proton::symbol descriptor("amqp:discharge:list"); + std::list vd; + vd.push_back(transaction_id); + vd.push_back(failed); + proton::value _value = vd; + send_ctrl(descriptor, _value); +} + +proton::tracker transaction_impl::send_ctrl(proton::symbol descriptor, proton::value _value) { + proton::value msg_value; + proton::codec::encoder enc(msg_value); + enc << proton::codec::start::described() + << descriptor + << _value + << proton::codec::finish(); + + + proton::message msg = msg_value; + proton::tracker delivery = txn_ctrl.send(msg); + return delivery; +} + +void transaction_impl::release_pending() { + for (auto d : pending) { + delivery d2(make_wrapper(unwrap(d))); + d2.release(); + } + pending.clear(); +} + +void transaction_impl::handle_outcome(proton::tracker t) { + pn_disposition_t *disposition = pn_delivery_remote(unwrap(t)); + proton::session session = t.session(); + if (state == State::DECLARING) { + // Attempting to declare transaction + proton::value val(pn_disposition_data(disposition)); + auto vd = get>(val); + if (vd.size() > 0) { + transaction_id = vd[0]; + state = State::DECLARED; + handler->on_session_open(session); + return; + } else if (pn_disposition_is_failed(disposition)) { + state = State::FREE; + transaction_delete(session); + // on_transaction_declare_failed + handler->on_session_error(session); + return; + } else { + state = State::FREE; + transaction_delete(session); + handler->on_session_error(session); + return; + } + } else if (state == State::DISCHARGING) { + // Attempting to commit/abort transaction + if (pn_disposition_is_failed(disposition)) { + if (!failed) { + state = State::FREE; + transaction_delete(session); + handler->on_session_transaction_commit_failed(session); + release_pending(); + return; + } else { + state = State::FREE; + transaction_delete(session); + // Transaction abort failed. + return; + } + } else { + if (failed) { + // Transaction abort is successful + state = State::FREE; + transaction_delete(session); + handler->on_session_transaction_aborted(session); + release_pending(); + return; + } else { + // Transaction commit is successful + state = State::FREE; + transaction_delete(session); + handler->on_session_transaction_committed(session); + return; + } + } + pending.clear(); + return; + } + throw proton::error("reached unintended state in local transaction handler"); +} + } // namespace proton diff --git a/cpp/src/transaction_handler.cpp b/cpp/src/transaction_handler.cpp new file mode 100644 index 000000000..bee861f9d --- /dev/null +++ b/cpp/src/transaction_handler.cpp @@ -0,0 +1,44 @@ +/* + * + * 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 "proton/transaction_handler.hpp" +#include "proton/delivery.h" +#include "proton/delivery.hpp" +#include "proton/message.hpp" +#include "proton/target_options.hpp" +#include "proton/tracker.hpp" +#include "proton/transfer.hpp" + +#include "proton_bits.hpp" +#include + +#include + +namespace proton { + +transaction_handler::~transaction_handler() = default; +void transaction_handler::on_transaction_declared(session) {} +void transaction_handler::on_transaction_committed(session) {} +void transaction_handler::on_transaction_aborted(session) {} +void transaction_handler::on_transaction_declare_failed(session) {} +void transaction_handler::on_transaction_commit_failed(session) {} + +} diff --git a/cpp/src/transaction_test.cpp b/cpp/src/transaction_test.cpp new file mode 100644 index 000000000..36c0c2a9b --- /dev/null +++ b/cpp/src/transaction_test.cpp @@ -0,0 +1,348 @@ +/* +* 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 +#include +#include +#include +#include +#include "proton/codec/decoder.hpp" +#include "proton/codec/encoder.hpp" +#include +#include +#include +#include +#include +#include +#include +#include "proton_bits.hpp" +#include +#include +#include +#include +#include // C++ API doesn't export disposition + + +#include "test_bits.hpp" // For RUN_ARGV_TEST and ASSERT_EQUAL + + +#include "types_internal.hpp" + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { +std::mutex m; +std::condition_variable cv; +bool listener_ready = false; +int listener_port; + + +const proton::binary fake_txn_id("prqs5678-abcd-efgh-1a2b-3c4d5e6f7g8e"); + + +} // namespace + + +class FakeBroker : public proton::messaging_handler { + private: + class listener_ready_handler : public proton::listen_handler { + void on_open(proton::listener& l) override { + std::lock_guard lk(m); + listener_port = l.port(); + listener_ready = true; + cv.notify_one(); + } + }; + + + std::string url; + listener_ready_handler listen_handler; + proton::receiver coordinator_link; + + + public: + proton::listener listener; + + + std::map> transactions_messages; + std::promise declare_promise; + std::promise commit_promise; + std::promise abort_promise; + + + FakeBroker(const std::string& s) : url(s) {} + + + void on_container_start(proton::container& c) override { + listener = c.listen(url, listen_handler); + } + + + void on_connection_open(proton::connection& c) override { + c.open(proton::connection_options{}.offered_capabilities({"ANONYMOUS-RELAY"})); + } + + + void on_receiver_open(proton::receiver& r) override { + // Identify the transaction link. + if(r.target().capabilities().size() > 0 && + r.target().capabilities()[0] == proton::symbol("amqp:local-transactions")) { + coordinator_link = r; + } + r.open(); + } + + + void on_message(proton::delivery& d, proton::message& m) override { + if (coordinator_link.active() && d.receiver() == coordinator_link) { + handle_transaction_control(d, m); + } else { + handle_application_message(d, m); + } + } + + + void handle_application_message(proton::delivery& d, proton::message& m) { + auto disp = pn_transactional_disposition(pn_delivery_remote(unwrap(d))); + if (disp != NULL) { + // transactional message + proton::binary txn_id = proton::bin(pn_transactional_disposition_get_id(disp)); + transactions_messages[txn_id].push_back(m); + } + } + + + void handle_transaction_control(proton::delivery& d, proton::message& m) { + proton::codec::decoder dec(m.body()); + proton::symbol descriptor; + proton::value _value; + + + proton::type_id _t = dec.next_type(); + if (_t == proton::type_id::DESCRIBED) { + proton::codec::start s; + dec >> s >> descriptor >> _value >> proton::codec::finish(); + } else { + std::cerr << "[fake_broker] Invalid transaction control message format: " << to_string(m) << std::endl; + d.reject(); + return; + } + + + if (descriptor == "amqp:declare:list") { + pn_delivery_t* pd = proton::unwrap(d); + pn_disposition_t* disp = pn_delivery_local(pd); + pn_custom_disposition_t *custom_disp = pn_custom_disposition(disp); + pn_custom_disposition_set_type(custom_disp, 0x33); + + + pn_data_t* pn_data = pn_custom_disposition_data(custom_disp); + pn_data_put_list(pn_data); + pn_data_enter(pn_data); + pn_data_put_binary(pn_data, pn_bytes(fake_txn_id.size(), reinterpret_cast(&fake_txn_id[0]))); + pn_data_exit(pn_data); + + + pn_delivery_settle(pd); + + + std::cout << "[BROKER] DECLARE handled" << std::endl; + declare_promise.set_value(); + + + } else if (descriptor == "amqp:discharge:list") { + // Commit / Abort transaction. + std::vector vd; + proton::get(_value, vd); + ASSERT_EQUAL(vd.size(), 2u); + proton::binary txn_id = vd[0].get(); + bool is_abort = vd[1].get(); + + + if (!is_abort) { + // Commit + std::cout << "[BROKER] COMMIT handled" << std::endl; + // As part of this test, we don't need to forward transactions_messages. + // We are leaving the messages here to count them later on. + commit_promise.set_value(); + d.accept(); + } else { + // Abort + std::cout << "[BROKER] ABORT handled" << std::endl; + transactions_messages.erase(txn_id); + abort_promise.set_value(); + d.accept(); + } + + + // Closing the connection as we are testing till commit/abort. + d.receiver().close(); + d.connection().close(); + listener.stop(); + } + } +}; + + +class test_send : public proton::messaging_handler { + private: + std::string server_address_; + int messages_left; + + + public: + proton::sender sender_; + std::promise transaction_finished_promise; + + + test_send(const std::string& s, int send_messages) : + server_address_(s), messages_left(send_messages) {} + + + void on_container_start(proton::container& c) override { + c.connect(server_address_); + } + + + void on_connection_open(proton::connection& c) override { + sender_ = c.open_sender("/test"); + } + + + void on_session_open(proton::session& s) override { + if (!s.transaction_is_declared()) { + s.transaction_declare(*this); + } else { + proton::binary got_txn_id = s.transaction_id(); + ASSERT_EQUAL(got_txn_id, fake_txn_id); + std::cout << "Client: Transaction declared successfully: " << got_txn_id << std::endl; + send(); + } + } + + + void on_sendable(proton::sender&) override { + send(); + } + + + void send() { + proton::session session = sender_.session(); + while (session.transaction_is_declared() && sender_.credit() && + messages_left > 0) { + proton::message msg("hello"); + sender_.send(msg); + messages_left--; + if(messages_left == 0) { + std::cout << "Client: Committing transaction." << std::endl; + session.transaction_commit(); + } + } + } + + + void on_session_transaction_committed(proton::session &s) override { + std::cout << "Client: Transaction committed" << std::endl; + transaction_finished_promise.set_value(); + s.connection().close(); + } + + + void on_session_transaction_aborted(proton::session &s) override { + std::cout << "Client: Transaction aborted" << std::endl; + transaction_finished_promise.set_value(); + s.connection().close(); + } +}; + + +int test_transaction_commit() { + const unsigned int messages_send = 5; + std::string broker_address("127.0.0.1:0"); + FakeBroker broker(broker_address); + + + proton::container broker_container(broker); + std::thread broker_thread([&broker_container]() -> void { broker_container.run(); }); + + + // Wait for the listener + std::unique_lock lk(m); + cv.wait(lk, [] { return listener_ready; }); + + + std::string server_address = "127.0.0.1:" + std::to_string(listener_port); + test_send send(server_address, messages_send); + std::future client_finish_future = send.transaction_finished_promise.get_future(); + + + proton::container client_container(send); + std::thread client_thread([&client_container]() -> void { client_container.run(); }); + + + std::cout << "Waiting for broker to process the commit..." << std::endl; + if (broker.commit_promise.get_future().wait_for(std::chrono::seconds(5)) == std::future_status::timeout) { + std::cerr << "Test FAILED: Broker did not process the commit in time." << std::endl; + broker_container.stop(); + client_container.stop(); + broker_thread.join(); + client_thread.join(); + return 1; + } + + + // Wait for client to confirm the action + std::cout << "Waiting for client to confirm the commit..." << std::endl; + if (client_finish_future.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) { + std::cerr << "Test FAILED: Client did not confirm the commit." << std::endl; + broker_container.stop(); + client_container.stop(); + broker_thread.join(); + client_thread.join(); + return 1; + } + + + // Only one transaction + ASSERT_EQUAL(broker.transactions_messages.size(), 1u); + // Check message count inside broker + ASSERT_EQUAL(broker.transactions_messages[fake_txn_id].size(), messages_send); + + + std::cout << "Test Passed." << std::endl; + broker_thread.join(); + client_thread.join(); + return 0; +} + + +int main(int argc, char** argv) { + int failed = 0; + RUN_ARGV_TEST(failed, test_transaction_commit()); + return failed; +} diff --git a/cpp/tests.cmake b/cpp/tests.cmake index b00d50129..85f1ae269 100644 --- a/cpp/tests.cmake +++ b/cpp/tests.cmake @@ -63,6 +63,9 @@ add_cpp_test(link_test) add_cpp_test(credit_test) add_cpp_test(delivery_test) add_cpp_test(context_test) +add_cpp_test(transaction_test) +target_link_libraries(transaction_test qpid-proton-core) + if (ENABLE_JSONCPP) add_cpp_test(connect_config_test) target_link_libraries(connect_config_test qpid-proton-core) # For pn_sasl_enabled