ZooKeeper
class, providing one method for each operation on the nodes. However, the shortcomings of this approach were quickly discovered.std::future
. We pass the request parameters to the client, the client returns an object representing the asynchronous calculation. When we call the std::future::get
method, we will be given control after the operation completes. If the operation fails, the call to std::future::get
should throw an exception.exists
methods: Stat exists(String path, boolean watch) void exists(String path, boolean watch, AsyncCallback.StatCallback cb, Object ctx) Stat exists(String path, Watcher watcher) void exists(String path, Watcher watcher, AsyncCallback.StatCallback cb, Object ctx)
future
, you have to add 2 more methods. A total of 6 methods, and this is only for one operation! I considered this unacceptable. void run(Command cmd, Callback callback);
CreateCmd
DeleteCmd
ExistsCmd
delete
operation takes a required path and can optionally take a version of the data to which it applies. The exists
operation also requires a path and can optionally take a function called when a node is deleted / created.delete
, setACL
, setData
), some accept an additional callback monitor, or they can publish events to the session callback monitor. You can implement these "templates" in the form of impurities (mixins) , from which we, as from bricks, will assemble our teams. In total, I managed to see 3 impurities:Pathable
- takes a path in the constructor and provides a method for getting the path.Versionable
- stores the version and provides methods for specifying the version and getting the specified version.Watchable
- stores and allows you to define a callback that is called when a node changes state.Versionable
: template <typename SelfType> struct Versionable { explicit Versionable(Version version = AnyVersion) : version_(version) {} SelfType & setVersion(Version version) { this->version_ = version; return static_cast<SelfType &>(*this); } Version version() const { return this->version_; } private: Version version_; };
setVersion
return the setVersion
base class Versionable
, the curiously recurring template pattern technique is used here. Adding impurities to teams is as follows: struct DeleteCmd : Pathable, Versionable<DeleteCmd> { explicit DeleteCmd(std::string path); // other methods };
using VoidCallback = std::function<void(std::error_code const&)>; using StatCallback = std::function<void(std::error_code const&, Stat const&)>; using ExistsCallback = std::function<void(std::error_code const&, bool, Stat const&)>; using StringCallback = std::function<void(std::error_code const&, std::string)>; using ChildrenCallback = std::function<void(std::error_code const&, Children, Stat const&)>; using DataCallback = std::function<void(std::error_code const&, std::string, Stat const&)>; using AclCallback = std::function<void(std::error_code const&, AclVector, Stat const&)>;
Stat
structure contains meta-information about a tree node, similar to the stat
structure on UNIX systems. For example, this structure contains the virtual time of the last modification, the size of the data stored in the node, the number of descendants, etc.CallbackType
. This is not quite beautiful, as the team begins to guess that it will be performed asynchronously with a callback, and this is exactly what we tried to avoid. However, I chose this particular implementation because of its simplicity and the fact that the asynchronous implementation is basic, and the remaining options will be a superstructure above it.Handle
- a low-level descriptor that hides all implementation details from the library client (for example, the fact that the command libzookeeper
used to execute commands). In C / C ++, this can be achieved by declaring a type, but not defining it in the library’s public header files: class Handle;
Handle
class is implemented is not so important. For simplicity, we can assume that this is in fact zhandle_t
from the libzookeeper
library, and the implementation of our commands in secret from the user converts the pointer to our incomplete type into a pointer to zhandle_t
. struct SomeCmd { using CallbackType = SomeCallbackType; void operator()(Handle *, CallbackType) const; };
class Session { public: // other methods template <typename CmdType> void run(CmdType const& cmd, typename CmdType::CallbackType callback) { cmd(this->getHandle(), std::move(callback)); } private: Handle * getHandle(); };
session.run(DeleteCmd("path").setVersion(knownVersion), myCallback);
run
.std::future
std::future
object, I want to have a function that has the following signature: template<class CmdType> std::future<ResultOf<CmdType>> runAsync(Session &, CmdType const&);
std::future
object representing the result of the asynchronous command execution.ResultOf
metafunction is ResultOf
. There are several ways to indicate the correspondence of function parameters to returned values, I chose the simplest one - just write out every possible case as a separate specialization of the template class DeduceResult
. template <typename CallbackType> struct DeduceResult; template <> struct DeduceResult<std::function<void(std::error_code const&)>> { using type = void; }; template <typename T> struct DeduceResult<std::function<void(std::error_code const&, T)>> { using type = typename std::decay<T>::type; }; template <typename T> struct DeduceResult<std::function<void(std::error_code const&, T, Stat const&)>> { using type = std::pair<typename std::decay<T>::type, Stat>; }; template <typename CmdType> using ResultOf = typename DeduceResult<typename CmdType::CallbackType>::type;
DeduceResult
is simple:void
.Stat
object is also transferred to the callback, the result and the Stat
object will be packed into std::pair
.ResultOf
is a template synonym ( alias template , one of the nice features of C ++ 11) that passes to the DeduceResult
type of callback defined in the command.std::decay
metafunction is noteworthy - some parameters are passed to the callback by reference, but we want to return them to customers by value, because objects can live on the stack and, if they are passed to another thread, references to them will already be destroyed by the time the client reads them.runAsync
. The implementation is almost obvious: you need to create the std::promise
object of the required type, get the std::future
object from it (by calling the std::promise::get_future()
method), create a special callback that will receive the std::promise
object and put in it the result or error of the callback. Then you just need to execute the command through the standard session interface with our callback. Since a promise
should have a callback, it is logical to make a callback a function object containing a promise
as a field. The resulting runAsync
function runAsync
looks like this: template <typename CmdType> std::future<ResultOf<CmdType>> runAsync(Session & session, CmdType const& cmd) { FutureCallback<ResultOf<CmdType>> cb; auto f = cb.getPromise().get_future(); session.run(cmd, std::move(cb)); return f; }
FutureCallback
function FutureCallback
in many ways mirrors the logic we nested in the ResultOf
metafunction. Based on the expected type of operation, we generate functions that pack our input arguments into an object and pass this object to a common (with a future
object) state via promise::set_value
or promise::set_exception
. template <typename T> void setError(std::promise>T> & p, std::error_code const& ec) { // codeToExceptionPtr . // , // exception_ptr, // . p.set_exception(codeToExceptionPtr(ec)); } template <typename T> struct CallbackBase { // promise , // std::function - . // . n337 (20.8.11.2.1) using PromisePtr = std::shared_ptr<std::promise<T>>; PromisePtr promisePtr; CallbackBase() : promisePtr(std::make_shared<std::promise<T>>()) {} std::promise<T> & getPromise() { return *promisePtr.get(); } }; template <typename T> struct FutureCallback : CallbackBase<T> { void operator()(std::error_code const& ec, T value) { if (ec) { setError(this->getPromise(), ec); } else { this->getPromise().set_value(std::move(value)); } } }; template <> struct FutureCallback<void> : CallbackBase<void> { void operator()(std::error_code const& ec) { if (ec) { setError(this->getPromise(), ec); } else { this->getPromise().set_value(); } } }; template >typename T> struct FutureCallback<std::pair<T, Stat>> : CallbackBase<std::pair<T, Stat>> { void operator()(std::error_code const& ec, T data, Stat const& stat) { if (ec) { setError(this->getPromise(), ec); } else { this->getPromise().set_value( std::make_pair(std::move(data), stat)); } } };
std::vector<std::future<std::string>> nodeNameFutures; // for (const auto & name : {"/node1", "/node2", "/node3"}) { nodeNameFutures.emplace_back(runAsync(session, CreateCmd(name))); } // for (auto & f : nodeNameFutures) { f.wait(); }
std::future
mechanism greatly simplifies such tasks. For example, I used this mechanism to implement the function of recursive deletion of a subtree. template <typename CmdType> ResultOf<CmdType> runSync(Session & session, CmdType const& cmd) { return runAsync(session, cmd).get(); }
Source: https://habr.com/ru/post/259307/
All Articles