Turi Create  4.0
comm_client.hpp
1 /* Copyright © 2017 Apple Inc. All rights reserved.
2  *
3  * Use of this source code is governed by a BSD-3-clause license that can
4  * be found in the LICENSE.txt file or at https://opensource.org/licenses/BSD-3-Clause
5  */
6 #ifndef CPPIPC_SERVER_COMM_CLIENT_HPP
7 #define CPPIPC_SERVER_COMM_CLIENT_HPP
8 #include <map>
9 #include <memory>
10 #include <chrono>
11 #include <core/parallel/atomic.hpp>
12 #include <core/logging/logger.hpp>
13 #include <boost/thread/thread.hpp>
14 #include <boost/thread/locks.hpp>
15 #include <boost/thread/lock_types.hpp>
16 #include <core/system/nanosockets/socket_errors.hpp>
17 #include <core/system/nanosockets/async_request_socket.hpp>
18 #include <core/system/nanosockets/subscribe_socket.hpp>
19 #include <core/system/cppipc/common/message_types.hpp>
20 #include <core/system/cppipc/common/status_types.hpp>
21 #include <core/system/cppipc/client/issue.hpp>
22 #include <core/system/cppipc/common/ipc_deserializer.hpp>
23 #include <core/system/cppipc/client/console_cancel_handler.hpp>
24 #include <core/system/exceptions/error_types.hpp>
25 #include <cctype>
26 #include <atomic>
27 
28 namespace cppipc {
29 namespace nanosockets = turi::nanosockets;
30 
31 std::atomic<size_t>& get_running_command();
32 std::atomic<size_t>& get_cancelled_command();
33 
34 class comm_client;
35 
36 namespace detail {
37 /**
38  * \ingroup cppipc
39  * \internal
40  * Internal utility function.
41  * Deserialized an object of a specific type from a reply message,
42  * and returns the result, clearing the message object.
43  * Works correctly for void types.
44  */
45 template <typename RetType, bool is_proxied_object>
47  static RetType exec(comm_client& client, reply_message& msg) {
48  turi::iarchive iarc(msg.body, msg.bodylen);
49  RetType ret = RetType();
50  iarc >> ret;
51  msg.clear();
52  return ret;
53  }
54 };
55 
56 
57 template <>
58 struct deserialize_return_and_clear<void, false> {
59  static void exec(comm_client& client, reply_message& msg) {
60  msg.clear();
61  return;
62  }
63 };
64 
65 
66 } // namespace detail
67 
69 
70 /**
71  * \ingroup cppipc
72  * The client side of the IPC communication system.
73  *
74  * The comm_client manages the serialization, and the calling of functions
75  * on remote machines. The comm_client and the comm_server reaches each other
76  * through the use of zookeeper. If both client and server connect to the same
77  * zookeeper host, and on construction are provided the same "name", they are
78  * connected.
79  *
80  * The comm_client provides communication capability for the \ref object_proxy
81  * objects. Here, we will go through an example of proxying the file_write class
82  * described in the \ref comm_server documentation.
83  *
84  * Basic Utilization
85  * -----------------
86  * Given a base class file_write_base, which is implemented on the server side,
87  * we can construct on the client side, an \ref object_proxy object which
88  * creates and binds to an implementation of file_write_base on the server side,
89  * and allows function calls across the network.
90  *
91  * We first repeat the description for the base class here:
92  * \code
93  * class file_write_base {
94  * public:
95  * virtual int open(std::string s) = 0;
96  * virtual void write(std::string s) = 0;
97  * virtual void close() = 0;
98  * virtual ~file_write_base() {}
99  *
100  * REGISTRATION_BEGIN(file_write_base)
101  * REGISTER(file_write_base::open)
102  * REGISTER(file_write_base::write)
103  * REGISTER(file_write_base::close)
104  * REGISTRATION_END
105  * };
106  * \endcode
107  *
108  * We can create an object_proxy by templating over the base class, and
109  * providing the comm_client object in the constructor.
110  * \code
111  * object_proxy<file_write_base> proxy(client);
112  * \endcode
113  * This will create on the remote machine, an instance of the file_write_impl
114  * object, and the object_proxy class provides the capability to call functions
115  * on the remote object. For instance, to call the "open" function, followed
116  * by some "writes" and "close".
117  * \code
118  * int ret = proxy.call(&file_write_base::open, "log.txt");
119  * proxy.call(&file_write_base::write, "hello");
120  * proxy.call(&file_write_base::write, "world");
121  * proxy.call(&file_write_base::close);
122  * \endcode
123  * The return type of the proxy.call function will match the return type of the
124  * member function pointer provided. For instance, &file_write_base::open
125  * returns an integer, and the result is forwarded across the network and
126  * returned.
127  *
128  * On destruction of the proxy object, the remote object is also deleted.
129  *
130  * Wrapping the Proxy Object
131  * -------------------------
132  * It might be convenient in many ways to wrap the object_proxy in a way to
133  * make it easy to use. This is a convenient pattern that is very useful.
134  *
135  * For instance, a proxy wrapper for the file_write_base object might look like:
136  * \code
137  * class file_write_proxy: public file_write_base {
138  * object_proxy<file_write_base> proxy;
139  * public:
140  * file_write_proxy(comm_client& comm): proxy(comm) { }
141  * int open(std::string s) {
142  * return proxy.call(&file_write_base::open, s);
143  * }
144  * void write(std::string s) {
145  * return proxy.call(&file_write_base::write , s);
146  * }
147  * void close() {
148  * return proxy.call(&file_write_base::close);
149  * }
150  * };
151  * \endcode
152  *
153  * Preprocessor Magic
154  * ------------------
155  * To facilitate the creation of base interfaces, calling REGISTER macros
156  * appropriately, and implementing the proxy wrapper, we provide the
157  * \ref GENERATE_INTERFACE, \ref GENERATE_PROXY and
158  * \ref GENERATE_INTERFACE_AND_PROXY magic macros.
159  *
160  * For instance, to generate the proxy and the base class for the above
161  * file_write_base object, we can write
162  *
163  * \code
164  * GENERATE_INTERFACE_AND_PROXY(file_write_base, file_write_proxy,
165  * (int, open, (std::string))
166  * (void, write, (std::string))
167  * (void, close, )
168  * )
169  * \endcode
170  *
171  * This is the recommended way to create proxy and base objects since this
172  * allows a collection of interesting functionality to be injected.
173  * For instance, this will allow functions to take base pointers as arguments
174  * and return base pointers (for instance file_write_base*). On the client side,
175  * proxy objects are serialized in a way so that the server side uses the
176  * matching implementation instance. When an object is returned, the object
177  * is registered on the server side and converted to a new proxy object
178  * on the client side. a
179  *
180  * Implementation Details
181  * ----------------------
182  * Many other details regarding safety when interfaces, or interface argument
183  * type modifications are described in the \ref comm_server documentation.
184  *
185  * The comm client internally maintains the complete mapping of all member
186  * function pointers to strings. The object_proxy class then has the simple
187  * task of just maintaining the object_ids: i.e. what remote object does it
188  * connect to.
189  *
190  * There is a special "root object" which manages all "special" tasks that
191  * operate on the comm_server itself. This root object always has object ID 0
192  * and is the object_factory_base. This is implemented on the server side by
193  * object_factory_impl, and on the client side as object_factory_proxy.
194  * The comm_client exposes the object_factory functionality as member functions
195  * in the comm_client itself. These are make_object(), ping() and
196  * delete_object()
197  */
198 class EXPORT comm_client {
199  private:
200  nanosockets::async_request_socket object_socket;
201  // This is a pointer because the endpoint address must be received from the
202  // server, so it cannot be constructed in the constructor
203  nanosockets::async_request_socket *control_socket = NULL;
204  nanosockets::subscribe_socket subscribesock;
205  turi::atomic<size_t> m_command_id;
206 
207  // a map of a string representation of the function pointer
208  // to the name. Why a string representation of a function pointer you ask.
209  // That is because a member function pointer is not always 8 bytes long.
210  // It can be 16 bytes long. Without any other knowledge, it is safer to keep it
211  // as a variable length object.
212  std::map<std::string, std::string> memfn_pointer_to_string;
213 
214  // status callbacks. Pairs of registered prefixes to the callback function
215  std::vector<std::pair<std::string,
216  std::function<void(std::string)> > > prefix_to_status_callback;
217 
218  boost::mutex status_callback_lock;
219 
220  boost::mutex ref_count_lock;
221  // a map of object IDs to the number of references they hold
222  std::map<size_t, size_t> object_ref_count;
223 
224  /**
225  * Issue a call to the remote machine.
226  * As a side effect, the call and reply message structures will be cleared.
227  * Returns 0 on success and a system error code on communication failure.
228  * Note that the reply_message may contain other cppipc errors.
229  */
230  int internal_call(call_message& call, reply_message& reply, bool control=false);
231 
232  int internal_call_impl(call_message& call,
233  nanosockets::zmq_msg_vector& ret,
234  bool control,
235  size_t timeout = 0);
236 
237  /** Checks is the pid set with set_server_alive_watch_pid is running.
238  * Sets server_running to false if pid is no longer running.
239  */
240  void poll_server_pid_is_running();
241 
242  /**
243  * Convert the auxiliary addresses we get back from the server to a real IP
244  * address if needed.
245  *
246  * This is only used for control and publish addresses.
247  */
248  std::string convert_generic_address_to_specific(std::string aux_addr);
249 
250  /**
251  * The root object (always object 0)
252  */
253  object_factory_proxy* object_factory;
254 
255  /**
256  * This thread repeatedly pings the server every 3 seconds, setting
257  * and clearing the flag server_alive as appropriate.
258  */
259  boost::thread* ping_thread;
260 
261  /**
262  * The lock / cv pair around the ping_thread_done value
263  */
264  boost::mutex ping_mutex;
265  boost::condition_variable ping_cond;
266 
267  /**
268  * Sets to true when the ping thread is done
269  */
270  volatile bool ping_thread_done = false;
271 
272  /**
273  * Server alive is true if the server is reachable some time in the
274  * last 3 pings. Server_alive is true on startup.
275  */
276  volatile bool server_alive = true;
277 
278  /**
279  * True if the socket is closed
280  */
281  bool socket_closed = false;
282 
283  /**
284  * The number of pings which have failed consecutively.
285  */
286  volatile size_t ping_failure_count = 0;
287 
288  size_t num_tolerable_ping_failures = 10;
289 
290  /**
291  * Callback issued when server reports status
292  */
293  void subscribe_callback(const std::string& msg);
294 
295  /**
296  * The point in time that must have passed for us to sync our tracked objects
297  * with the server.
298  */
299  std::chrono::steady_clock::time_point object_sync_point;
300 
301  /**
302  * If set, the control address to use.
303  */
304  std::string alternate_control_address;
305 
306  /**
307  * If set, the publish address to use.
308  */
309  std::string alternate_publish_address;
310 
311  /**
312  * Set to true when the client is started. False otherwise.
313  */
314  bool started = false;
315 
316  /**
317  * The name this client was told to connect to.
318  */
319  std::string endpoint_name;
320 
321  /**
322  * The signal handler that was in effect before this client was established
323  */
324  bool cancel_handling_enabled = false;
325 
326  /**
327  * The pid to watch to test if the server is alive
328  */
329  int32_t server_alive_watch_pid = 0;
330 
331  public:
332 
333  /**
334  * Constructs a comm client which uses remote communication
335  * via zookeeper/zeromq. The client may find the remote server via either
336  * zookeeper (in which case zkhosts must be a list of zookeeper servers, and
337  * name must be a unique key value), or you can provide the address
338  * explicitly. Note that if a server is listening via zookeeper, the client
339  * MUST connect via zookeeper; providing the server's actual tcp bind address
340  * will not work. And similarly if the server is listening on an explicit
341  * zeromq endpoint address and not using zookeeper, the client must connect
342  * directly also without using zookeeper.
343  *
344  * After construction, authentication methods can be added then \ref start()
345  * must be called to initiate the connection.
346  *
347  * \param zkhosts The zookeeper hosts to connect to. May be empty. If empty,
348  * the "name" parameter must be a zeromq endpoint address to
349  * bind to.
350  * \param name The key name to connect to. This must match the "name" argument
351  * on construction of the remote's comm_server. If zkhosts is
352  * empty, this must be a valid zeromq endpoint address.
353  * \param num_tolerable_ping_failures The number of allowable consecutive ping
354  * failures before the server is considered dead.
355  * \param alternate_publish_address This should match the
356  * "alternate_publish_address" argument on construction of the
357  * remote's comm_server. If zkhosts is empty, this must be a valid
358  * zeromq endpoint address. This can be empty, in which case the client
359  * will ask the server for the appropriate address. It is recommended
360  * that this is not specified.
361  */
362  comm_client(std::vector<std::string> zkhosts,
363  std::string name,
364  size_t num_tolerable_ping_failures = (size_t)(-1),
365  std::string alternate_control_address="",
366  std::string alternate_publish_address="",
367  const std::string public_key = "",
368  const std::string secret_key = "",
369  const std::string server_public_key = "",
370  bool ops_interruptible = false);
371 
372  /**
373  * Constructs an inproc comm_client. The inproc comm_client
374  * and comm_server are required to have the same zmq_ctx.
375  *
376  * \param name The inproc socket address, must start with inproc://
377  */
378  comm_client(std::string name, void* zmq_ctx);
379 
380  /**
381  * Sets a pid to watch. If this pid goes away, server is considered dead.
382  * This is a more robust way compared to "pings" for local interprocess
383  * communication. Set to 0 to disable.
384  */
385  void set_server_alive_watch_pid(int32_t pid);
386 
387  /**
388  * Initialize the comm_client, called right inside the constructor.
389  */
390  void init(bool ops_interruptible = false);
391 
392  /**
393  * Initializes connections with the servers
394  * Must be called prior to creation of any client objects.
395  * Returns reply_status::OK on success, and an error code failure.
396  * Failure could be caused by an inability to connect to the server, or
397  * could also be due to authentication errors.
398  */
399  reply_status start();
400 
401  /**
402  * Destructor. Calls stop if not already called
403  */
404  ~comm_client();
405 
406  /**
407  * Stops the comm client object. Closes all open sockets
408  */
409  void stop();
410 
411  /**
412  * Creates an object of a given type on the remote machine.
413  * Returns an object ID. If return value is (-1), this is a failure.
414  *
415  * \note This call redirects to the object_factory_proxy
416  */
417  size_t make_object(std::string object_type_name);
418 
419  /**
420  * Ping test. Sends a string to the remote system,
421  * and replies with the same string.
422  *
423  * \note This call redirects to the object_factory_proxy
424  */
425  std::string ping(std::string);
426 
427  /**
428  * Delete object. Deletes the object with ID objectid on the remote machine.
429  *
430  * \note This call redirects to the object_factory_proxy
431  */
432  void delete_object(size_t objectid);
433 
434  /**
435  * Functions for manipulating local reference counting data structure
436  */
437  size_t incr_ref_count(size_t object_id);
438 
439  size_t decr_ref_count(size_t object_id);
440 
441  size_t get_ref_count(size_t object_id);
442 
443  /**
444  * This thread is used to serve the status callbacks.
445  * This prevents status callback locks from blocking the server
446  */
447  boost::thread* status_callback_thread = NULL;
448  /**
449  * The lock / cv pair around the ping_thread_done value
450  */
451  boost::mutex status_buffer_mutex;
452  boost::condition_variable status_buffer_cond;
453  std::vector<std::string> status_buffer;
454  bool status_callback_thread_done = false;
455 
456  /** The function which implements the thread which
457  * issues the messages to
458  * the status callback handlers.
459  */
460  void status_callback_thread_function();
461 
462  /**
463  * Terminates the thread which calls the callback handlers. Unprocessed
464  * messages are dropped.
465  */
466  void stop_status_callback_thread();
467 
468  /**
469  * Starts the status callback thread if not already started.
470  */
471  void start_status_callback_thread();
472 
473  /**
474  * Adds a callback for server status messages.
475  * The callback will receive all messages matching the specified prefix.
476  * For instance:
477  * \code
478  * client.add_status_watch("A", callback);
479  * \endcode
480  *
481  * will match all the following server messages
482  * \code
483  * server.report_status("A", "hello world"); // emits A: hello world
484  * server.report_status("ABC", "hello again"); // emits ABC: hello again
485  * \endcode
486  *
487  * On the other hand
488  * \code
489  * client.add_status_watch("A:", callback);
490  * \endcode
491  * will only match the first.
492  *
493  * Callbacks should be processed relatively quickly and should be thread
494  * safe. Multiple callbacks may be processed simultaneously in different
495  * threads. Callback function also should not call \ref add_status_watch
496  * or \ref remove_status_watch, or a deadlock may result.
497  *
498  * If multiple callbacks are registered for exactly the same prefix, only the
499  * last callback is recorded.
500  *
501  * \note The current prefix checking implementation is not fast, and is
502  * simply linear in the number of callbacks registered.
503  */
504  void add_status_watch(std::string watch_prefix,
505  std::function<void(std::string)> callback);
506 
507  /**
508  * Removes a status callback for a given prefix. Note that the function
509  * associated with the prefix may still be called even after this function
510  * returns. To ensure complete removal of the function,
511  * stop_status_callback_thread() and start_status_callback_thread()
512  * must be called.
513  */
514  void remove_status_watch(std::string watch_prefix);
515 
516  /**
517  * Clears all status callbacks.
518  * Note that status callbacks may still be called even after this function
519  * returns. To ensure complete removal of the function,
520  * stop_status_callback_thread() and start_status_callback_thread()
521  * must be called.
522  */
523  void clear_status_watch();
524 
525  /**
526  * Stops the ping thread.
527  */
528  void stop_ping_thread();
529 
530  /**
531  * Tries to synchronize the list of tracked objects with the server by
532  * sending a list of objects to be deleted.
533  * Returns 0 on success, -1 on failure.
534  */
535  int send_deletion_list(const std::vector<size_t>& object_ids);
536 
537 
538  /**
539  * \internal
540  * Registers a member function which then can be used in the \ref call()
541  * function
542  */
543  template <typename MemFn>
544  void register_function(MemFn f, std::string function_string) {
545  // It seems like the function pointer itself is insufficient to identify
546  // the function. Append the type of the function.
547  std::string string_f(reinterpret_cast<const char*>(&f), sizeof(MemFn));
548  string_f = string_f + typeid(MemFn).name();
549  if (memfn_pointer_to_string.count(string_f) == 0) {
550  memfn_pointer_to_string[string_f] = function_string;
551  //std::cerr << "Registering function " << function_string << "\n";
552  }
553  }
554 
555  template <typename MemFn>
556  void prepare_call_message_structure(size_t objectid, MemFn f, call_message& msg) {
557  // try to find the function
558  // It seems like the function pointer itself is insufficient to identify
559  // the function. Append the type of the function.
560  std::string string_f(reinterpret_cast<char*>(&f), sizeof(MemFn));
561  string_f = string_f + typeid(MemFn).name();
562  if (memfn_pointer_to_string.count(string_f) == 0) {
563  throw ipcexception(reply_status::NO_FUNCTION);
564  }
565  msg.objectid = objectid;
566  msg.function_name = memfn_pointer_to_string[string_f];
567  // trim the function call printing to stop at the first space
568 // std::string trimmed_function_name;
569 // std::copy(msg.function_name.begin(),
570 // std::find(msg.function_name.begin(), msg.function_name.end(), ' '),
571 // std::inserter(trimmed_function_name, trimmed_function_name.end()));
572  //std::cerr << "Calling object " << objectid << " function " << trimmed_function_name << "\n";
573  }
574 
575  /**
576  * Calls a remote function returning the result.
577  * The return type is the actual return value.
578  * May throw an exception of type reply_status on failure.
579  *
580  * NOTE: ONLY the main thread can call this. If this becomes untrue, some
581  * invariants will be violated (only one thread is allowed to change the
582  * currently running command).
583  */
584  template <typename MemFn, typename... Args>
585  typename detail::member_function_return_type<MemFn>::type
586  call(size_t objectid, MemFn f, const Args&... args) {
587  if (!started) {
588  throw ipcexception(reply_status::COMM_FAILURE, 0, "Client not started");
589  }
590  typedef typename detail::member_function_return_type<MemFn>::type return_type;
591  call_message msg;
592  prepare_call_message_structure(objectid, f, msg);
593  // generate the arguments
594  turi::oarchive oarc;
595  cppipc::issue(oarc, f, args...);
596  /*
597  * Complete hack.
598  * For whatever reason zeromq's zmq_msg_send and zmq_msg_recv function
599  * return the size of the mesage sent in an int. Even though the message
600  * size can be size_t.
601  * Also, zmq_msg_send/zmq_msg_recv use "-1" return for failure, thus
602  * bringing up the issue of integer overflow just "coincidentally" hitting
603  * -1 and thus failing terribly terribly.
604  * Solution is simple. Pad the buffer to even.
605  */
606  if (oarc.off & 1) oarc.write(" ", 1);
607  msg.body = oarc.buf;
608  msg.bodylen = oarc.off;
609 
610  // Set the command id
611  // 0 and uint64_t(-1) have special meaning, so don't send those
612  size_t command_id = m_command_id.inc();
613  auto ret = msg.properties.insert(std::make_pair<std::string, std::string>(
614  "command_id", std::to_string(command_id)));
615  ASSERT_TRUE(ret.second);
616 
617  auto &r = get_running_command();
618  r.store(command_id);
619 
620  // Read and save the current signal handler (e.g. Python's SIGINT handler)
621  // Set signal handler to catch CTRL-C during this call
622  if(cancel_handling_enabled &&
623  !console_cancel_handler::get_instance().set_handler()) {
624  logstream(LOG_WARNING) << "Could not read previous signal handler, "
625  "thus will not respond to CTRL-C.\n";
626  cancel_handling_enabled = false;
627  }
628 
629  // call
630  reply_message reply;
631  int retcode = internal_call(msg, reply);
632 
633  // Replace the SIGINT signal handler with the original one
634  if(cancel_handling_enabled) {
635  if(!console_cancel_handler::get_instance().unset_handler()) {
637  "Could not reset signal handler after server operation. Disabling CTRL-C support.\n";
638  cancel_handling_enabled = false;
639  }
640  }
641 
642  // Check if we need to re-raise a SIGINT in Python
643  if(cancel_handling_enabled) {
644  // Check if CTRL-C was pressed for this command.
645  size_t running_command = get_running_command().load();
646  if(running_command && running_command == get_cancelled_command().load()) {
647  // Check if there was a cancel message, whether it was heeded.
648  auto ret = reply.properties.find(std::string("cancel"));
649 
650  // If there is no 'cancel' property, then must_cancel was never checked
651  // on the server side, showing that this command does not support it.
652  if(ret == reply.properties.end()) {
653  // Raise this again for Python to make sure you can break out of a for
654  // loop with a turicreate call inside that does not throw on ctrl-c
655  // NOTE: I don't think I care if raise fails.
656  console_cancel_handler::get_instance().raise_cancel();
657  }
658  }
659  }
660 
661  // Reset running command
662  get_running_command().store(0);
663 
664  bool success = (retcode == 0);
665  std::string custommsg;
666  if (reply.body != NULL && reply.bodylen > 0) {
667  custommsg = std::string(reply.body, reply.bodylen);
668  }
669  if (!success) {
670  throw ipcexception(reply_status::COMM_FAILURE, retcode, custommsg);
671  } else if (reply.status != reply_status::OK) {
672  switch(reply.status) {
674 #ifdef COMPILER_HAS_IOS_BASE_FAILURE_WITH_ERROR_CODE
675  throw(std::ios_base::failure(custommsg, std::error_code()));
676 #else
677  throw(std::ios_base::failure(custommsg));
678 #endif
680  throw std::out_of_range(custommsg);
682  throw turi::bad_alloc(custommsg);
684  throw turi::bad_cast(custommsg);
685  default:
686  throw ipcexception(reply.status, retcode, custommsg);
687  }
688  } else {
689  detail::set_deserializer_to_client(this);
690  return detail::deserialize_return_and_clear<return_type,
691  std::is_convertible<return_type, ipc_object_base*>::value>::exec(*this, reply);
692  }
693  }
694 };
695 
696 } // cppipc
697 #endif
#define logstream(lvl)
Definition: logger.hpp:276
boost::mutex status_buffer_mutex
The serialization input archive object which, provided with a reference to an istream, will read from the istream, providing deserialization capabilities.
Definition: iarchive.hpp:60
std::map< std::string, std::string > properties
The status of the call.
detail::member_function_return_type< MemFn >::type call(size_t objectid, MemFn f, const Args &... args)
std::string function_name
the object to call
void clear()
Empties the message, freeing all contents.
Call was successful.
#define LOG_WARNING
Definition: logger.hpp:98
std::string delete_object(std::string s3_url, std::string proxy="")
void register_function(MemFn f, std::string function_string)
#define ASSERT_TRUE(cond)
Definition: assertions.hpp:309
The function requested did not exist.
std::map< std::string, std::string > properties
the function to call on the object
size_t bodylen
The serialized arguments of the call. May point into bodybuf.
void write(const char *c, std::streamsize s)
Definition: oarchive.hpp:118
size_t bodylen
The serialized contents of the reply. May point into bodybuf.
The serialization output archive object which, provided with a reference to an ostream, will write to the ostream, providing serialization capabilities.
Definition: oarchive.hpp:80
void issue(turi::oarchive &msg, MemFn fn, const Args &... args)
Definition: issue.hpp:79