Flow 1.0.2
Flow project: Public API.
Todo List
Namespace flow

As of this writing the exact nature of where the project will permanently reside (and who will maintain it vs. use it) is in flux. Therefore for now I have removed the section covering certain topics and replaced it with the to-do you're reading. This should be undone when things settle down (obviously ensuring the brought-back section is made accurate). The topics to cover: "@author" (including contact info); GitHub/other address indicating where to browse the project source; link(s) to the latest auto-generated web docs (if any); a section on the history of the project; and licensing info (if deemed needed) or pointer to it. (Reminder: Also update any similar sections of the historically significant net_flow::Node doc header.)

Since Flow gained its first users beyond the original author, some Flow-adjacent code has been written from which Flow can benefit, including a potential io module/namespace for general networking/local I/O. (Flow itself continued to be developed, but some features were added elsewhere for expediency; this is a reminder to factor them out into Flow for the benefit of all.) Some features to migrate here might be: boost.asio extensions to UDP receive APIs to obtain receipt time stamps and destination IP (recvmsg() with ancillary data extensions) and to receive multiple datagrams in one call (recvmmsg()); boost.asio-adjacent facility to add custom socket options similarly to how boost.asio does it internally; boost.asio support for (local) transmission of native socket handles and security data over stream sockets (SCM_RIGHTS, etc.).

Member flow::async::Concurrent_task_loop::schedule_from_now (const Fine_duration &from_now, Scheduled_task &&task)=0
Deal with the scheduled-tasks-affected-from-outside-loop corner case of the Concurrent_task_loop::schedule_*() APIs. Perhaps add bool in_loop_use_only arg which, if false, will always disable the single_threaded optimization internally. At this time it always enables it if n_threads() == 1 which will cause thread un-safety if the returned handle is touched from outside an in-loop task. void versions of the schedule_*() APIs should be added which would lack this, as in that case there is no handle to misuse outside the loop.
Member flow::async::Concurrent_task_loop::start (Task &&init_task_or_empty=Task(), const Thread_init_func &thread_init_func_or_empty=Thread_init_func())=0
Concurrent_task_loop::start() has an optional thread-initializer-function arg; it could be reasonable to ass a thread-finalizer-function arg symmetrically. As of this writing there is no use case, but it's certainly conceivable.
Member flow::async::Concurrent_task_loop::stop ()=0
boost.asio has advanced features that might help to mark certain tasks as "must-run if already queued, even if one `stop()`s"; consider providing user-friendly access to these features, perhaps in the context of the existing Concurrent_task_loop::stop() API. These features are documented as of this writing at least in the io_context doc page.
Class flow::async::Cross_thread_task_loop
Add dynamic configurability of low-level thread/core behavior of Cross_thread_task_loop, Segregated_thread_task_loop, and the Concurrent_task_loop interface generally, as these parameters (including n_threads_or_zero, est_hw_core_sharing_helps_algo, est_hw_core_pinning_helps_algo) can only be set at construction time even though start() and stop() can be invoked anytime. For instance a non-virtual configure() method could be added to each Concurrent_task_loop subclass, potentially with different signatures. Note, also, that the decision to have stop() exit the actual threads, in addition to making some things easier to code/reason about internally, is also more amenable to these dynamic changes – messing with threading behavior dynamically is easier if one can exit and re-spawn the thread pool.
Member flow::async::optimize_pinning_in_thread_pool (flow::log::Logger *logger_ptr, const std::vector< util::Thread * > &threads_in_pool, bool est_hw_core_sharing_helps_algo, bool est_hw_core_pinning_helps_algo, bool hw_threads_is_grouping_collated)
For the Darwin/Mac platform only: There is likely a bug in optimize_pinning_in_thread_pool() regarding certain low-level pinning calls, the effect of which is that this function is probably effectively a no-op for now in Macs. The bug is explained inside the body of the function.
Member flow::async::S_ASYNC_AND_AWAIT_CONCURRENT_COMPLETION
Much like the promise/future mechanism provides optional timed wait functionality, it might make sense to provide the API ability to set an optional time limit for any wait invoked by Synchronicity::S_ASYNC_AND_AWAIT_CONCURRENT_COMPLETION or Synchronicity::S_ASYNC_AND_AWAIT_CONCURRENT_START. Probably best to add this only once a need clearly arises though.
Member flow::async::Single_thread_task_loop::schedule_from_now (const Fine_duration &from_now, Scheduled_task &&task)
Deal with the scheduled-tasks-affected-from-outside-loop corner case of the Single_thread_task_loop::schedule_*() APIs. Perhaps add bool in_loop_use_only arg which, if false, will always disable the single_threaded optimization internally. At this time it always enables it which will cause thread un-safety if the returned handle is touched from outside an in-loop task. void versions of the schedule_*() APIs should be added which would lack this, as in that case there is no handle to misuse outside the loop.
Class flow::cfg::Config_manager< S_d_value_set >
Dynamic config support lacks crash rejection; though these features should be an incremental improvement around the existing code. By crash rejection I mean something like: config file X comes in; we rename it to X.unparsed; we try to parse it; program crashes – versus it's fine, so we rename it X.parsed, and next time X.parsed is what we presumably-safely parse after any restart. Similarly invalid config may not cause a crash but still shouldn't be repeatedly re-parsed (etc.). Exact design TBD and will be informed by previous work on other projects (by echan and ygoldfel at least).
Member flow::cfg::Config_manager< S_d_value_set >::apply_static (const fs::path &static_cfg_path, const typename Final_validator_func< Value_set >::Type &... final_validator_func, bool commit=true)
Add support for command line as a config source in addition to file(s), for static config in cfg::Config_manager and cfg::Static_config_manager.
Class flow::cfg::Dynamic_cfg_context< Root, Target, Target_ptr >
flow::cfg::Dynamic_cfg_context Target_ptr is deprecated and shall be always left at its default value in future code; eventually remove it entirely and hard-code the default value internally.
Class flow::cfg::Option_set< Value_set >
Add individual-option-changed detection API(s) in addition to the existing Option_set overall-value-set-changed detection API. This is contingent on a use case needing it. The existing code already detects this internally and logs a message for each changed option (in canonicalize_candidate()).
Member flow::cfg::Static_config_manager< Value_set >::apply (const fs::path &cfg_path, const typename Final_validator_func< Value_set >::Type &final_validator_func)
Add support for command line as a config source in addition to file(s), for static config in cfg::Config_manager.
Class flow::log::Async_file_logger
Lacking feature: Compress-as-you-log in Async_file_logger. So, optionally, when characters are actually written out to file-system, gzip/zip/whatever them instead of writing plain text. (This is possible at least for gzip.) Background: It is common-place to compress a log file after it has been rotated (e.g., around rotation time: F.log.1.gz -> F.log.2.gz, F.log -> F.log.1 -> F.log.1.gz). It is more space-efficient (at least), however, to write to F.log.gz directly already in compressed form; then rotation requires only renaming (e.g.: F.log.1.gz -> F.log.2.gz, F.log.gz [already gzipped from the start] -> F.log.1.gz).
Member flow::log::Async_file_logger::Async_file_logger (Logger *backup_logger_ptr, Config *config, const fs::path &log_path, bool capture_rotate_signals_internally)
Consider adding Async_file_logger constructor option to overwrite the file instead of appending.
Member flow::log::Async_file_logger::log_flush_and_reopen (bool async=true)
Async_file_logger::log_flush_and_reopen(true) is great for flushing, such as in an abort-signal handler, but providing just the flushing part without the reopening might be useful. At the moment we've left it this way, due to the vague feeling that closing the file upon flushing it is somehow more final and thus safer (in terms of accomplishing its goal) in an abort-signal scenario. Feelings aren't very scientific though.
Class flow::log::Config
Class Config doc header is wordy and might be hard to follow; rewrite for clarity/flow/size.
Member flow::log::Config::Config (Config &&)=delete
Reconsider providing a Config move constructor. I just didn't need to deal with it.
Member flow::log::Config::operator= (Config &&)=delete
Reconsider providing a Config move assignment. I just didn't need to deal with it.
Member flow::log::Config::operator= (const Config &)=delete
Reconsider providing a Config copy assignment. I just didn't need to deal with it.
Member flow::log::Logger::set_thread_info (std::string *call_thread_nickname, flow::util::Thread_id *call_thread_id)
It would be more consistent to rename set_thread_info() to this_thread_set_info(), since it operates in thread-local fashion. This was a naming convention oversight.
Member flow::log::Logger::set_thread_info_in_msg_metadata (Msg_metadata *msg_metadata)
It would be more consistent to rename set_thread_info_in_msg_metadata() to this_thread_set_info_in_msg_metadata(), since it operates in thread-local fashion. This was a naming convention oversight.
Member flow::log::Logger::this_thread_set_logged_nickname (util::String_view thread_nickname=util::String_view(), Logger *logger_ptr=0, bool also_set_os_name=true)
this_thread_set_logged_nickname() could take multiple Loggers, since it is an app-wide function and potentially would want to be reflected in multiple loggers. It could take a pair of iterators or a container (in both cases, of template argument type). Arguably, though, this is overkill; as (1) most code paths deal with only one get_logger()-owning object each; (2) naming of a thread is obvious enough in subsequent logs (hence this message only calls attention to that operation plus notes the thread ID before the nickname assignment, not necessarily the most critical of information). Certainly this zero-to-one-Logger version must continue to be available for syntactic-sugary convenience, even if the to-do is performed.
Class flow::log::Msg_metadata
Add support in Msg_metadata for a message ID which could more straightforwardly help the human log reader to map a log line to the originating log call site in source code. One approach, then, might be to output that message ID when available; else output m_msg_src_file, m_msg_src_line, m_msg_src_function; or maybe both.
Class flow::net_flow::asio::Node
To enable reactor-style async_*() operations, meaning waiting for readability/writability/etc. but not performing the actual operation before calling the user handler, we provide a null_buffers-style interface; like newer boost.asio versions we should deprecate this in favor of simpler async_wait() APIs. This would apply to net_flow::asio::Peer_socket and net_flow::asio::Server_socket APIs. Similarly consider doing this for the sync_*() operations in the non-asio superclasses net_flow::Peer_socket and net_flow::Server_socket. Note that Event_set::async_wait() and Event_set::sync_wait() already exist; they are powerful but a bit complex to use in these simple situations. Hence the following hypothetical wrappers would be welcome replacements for the deprecated null_buffers and "reactor-style" APIs in these classes: net_flow::Peer_socket::sync_wait(Event_set::Event_type), net_flow::asio::Peer_socket::async_wait(Event_set::Event_type), net_flow::Server_socket::sync_wait(), net_flow::Server_socket::async_wait(). (Timeout-arg versions would be desired also, as they exist now.)
Member flow::net_flow::Event_set::emit_result_sockets (Sockets *target_set, Event_type ev_type, Error_code *err_code=0)
Event_set::emit_result_sockets() sets a Sockets structure which stores boost:anys each of which stores either a Peer_socket::Ptr or a Server_socket::Ptr; Sockets should be changed to store C++17 std::variants. Performance, both internally and externally, would improve by using this type-safe compile-time mechanism (which is akin to unions but much more pleasant to use). At the time this feature was written, Flow was in C++11, so variants were not available, and the author wanted to play around with anys instead of haxoring old-school unions. variant is much nicer, however, and the dynamic nature of any is entirely unnecessary here.
Class flow::net_flow::Net_env_simulator
One thing we should probably add for a bit of realism is the following. If a loss range [A, B] is specified, an individual packet's simulated latency will be a uniformly random number in that range. Because of this there will be frequent re-ordering of packets: if range is [50, 100] ms, and we physically get packet X at time 0 and packet Y at time 5 ms, and the simulated latencies are 80, 60 ms, respectively, then Y will "arrive" 80 - 60 - 5 = 15 ms BEFORE X despite being sent earlier (assuming perfect actual network conditions under the simulation). In reality, packets aren't re-ordered all that often. So if we wanted to simulate a high latency without re-ordering, we'd have to set the range to [A, A] for some A. To avoid having to do that, we can add some internal memory into Net_env_simulator that would put a floor on a randomly generated latency. In the previous example, the range for packet Y would be chosen as [75, 100] (instead of [50, 100]) to guarantee that packet Y arrives at least a bit later than packet X. Of course that might skew the latency range in bursty traffic, but that might be an OK behavior. We could add a knob for how often to ignore this new logic and allow a re-ordering, so that that is also simulated.
Class flow::net_flow::Node

Based on some outside experience, there maybe be problems – particularly considering the to-do regarding dual-stack IPv6/v4 support – in servers listening in multiple-IP situations; make sure we support these seamlessly. For example consider a machine with a WAN IP address and a LAN (10.x.x.x) IP address (and possibly IPv6 versions of each also) that (as is typical) binds on all of them at ANY:P (where P is some port; and ANY is the IPv6 version, with dual-stack mode ensuring V4 datagrams are also received). If a client connects to a LAN IP, while in our return datagrams we set the source IP to the default, does it work? Outside experience shows it might not, depending, plus even if in our protocol it does, it might be defeated by some firewall... the point is it requires investigation (e.g., mimic TCP itself; or look into what IETF or Google QUIC does; and so on).

Actively support IPv6 and IPv4, particularly in dual-stack mode (wherein net_flow::Server_socket would bind to an IPv6 endpoint but accept incoming V4 and V6 connections alike). It already supports it nominally, in that one can indeed listen on either type of address and connect to either as well, but how well it works is untested, and from some outside experience it might involve some subtle provisions internally.

Some of the ostream<< operators we have take X* instead of const X&; this should be changed to the latter for various minor reasons and for consistency.

flow::net_flow should use flow::cfg for its socket-options mechanism. It is well suited for that purpose, and it uses some ideas from those socket-options in the first place but is generic and much more advanced. Currently net_flow socket-options are custom-coded from long before flow::cfg existed.

ostream output operators for Node and asio::Node should exist. Also scour through all types; possibly some others could use the same. (I have been pretty good at already implementing these as-needed for logging; but I may have "missed a spot.")

Member flow::net_flow::Node::~Node () override

Server_socket objects closed as if by what?

Provide another method to shut down everything gracefully?

Class flow::net_flow::Peer_socket
Closing connection considerations. May implement closing only via timeouts at first (as opposed to explicit closing). Below text refers to close_final() and close_start(), but those are just ideas and may be replaced with timeout, or nothing. At this time, the only closing supported is abrupt close due to error or abrupt close via close_abruptly().
Member flow::net_flow::Peer_socket::close_abruptly (Error_code *err_code=0)

close_abruptly() return bool (false on failure)?

Currently this close_abruptly() is the only way for the user to explicitly close one specified socket. All other ways are due to error (or other side starting graceful shutdown, once we implement that). Once we implement graceful close, via close_start() and close_final(), use of close_abruptly() should be discouraged, or it may even be deprecated (e.g., Nodes lack a way to initiate an abrupt close for a specific socket).

Member flow::net_flow::Peer_socket::info () const
Provide a similar info() method that loads an existing structure (for structure reuse).
Member flow::net_flow::Peer_socket::options () const
Provide a similar options() method that loads an existing structure (for structure reuse).
Member flow::net_flow::Peer_socket_info::m_snd_cong_ctl_in_flight_bytes
Does m_snd_cong_ctl_in_flight_bytes count data queued in the pacing module or truly In-flight data only?
Member flow::net_flow::Peer_socket_options::m_st_cong_ctl_max_cong_wnd_blocks
Reconsider this value after Receive window feature is implemented.
Member flow::net_flow::Peer_socket_options::m_st_max_block_size
max-block-size should be dynamically determined (but overridable by this option). That is a complex topic, however, with considerations such as MTU discovery, ICMP errors, and who knows what else.
Member flow::net_flow::Peer_socket_send_stats::m_init_time
Peer_socket_receive_stats also independently stores a similar value, so to save memory put m_init_time elsewhere.
Class flow::net_flow::Server_socket
Implement Server_socket::close() functionality – at least the equivalent of Peer_socket::close_abruptly().
Member flow::net_flow::Server_socket::accept (Error_code *err_code=0)
Reconsider allowing successful accept() in State::S_CLOSING state?
Member flow::perf::Clock_type
Consider adding a system-calendar-clock (a/k/a POSIX time) type to perf::Clock_type. It would be a cousin of Clock_type::S_REAL_HI_RES. It would certainly be inferior in terms of resolution/monotonicity/etc., and one would think S_REAL_HI_RES would always be preferable. Nevertheless it would be interesting to "officially" see its characteristics including in particular (1) resolution and (2) its own perf cost especially vs. S_REAL_HI_RES which we know is quite fast itself. This may also help a certain to-do listed as of this writing in the doc header of flow::log FLOW_LOG_WITHOUT_CHECKING() (the main worker bee of the log system, the one that generates each log time stamp).
Member flow::perf::operator*= (Duration_set &target, uint64_t mult_scale)
Maybe allow operator*=(Duration_set) by a potentially negative number; same for division.
Member flow::perf::timed_function (Clock_type clock_type, Accumulator *accumulator, Func &&function)

timed_function() overload exists for a single Clock_type, but simultaneous multi-clock timing using the perf::Clock_types_subset paradigm (as used, e.g., in Checkpointing_timer) would be a useful and consistent API. E.g., one could measure user and system elapsed time simultaneously. As of this writing this only does not exist due to time constraints: a perf-niggardly version targeting one clock type was necessary.

timed_function(), when operating on an atomic<duration_rep_t>, uses += for accumulation which may be lock-free but uses strict ordering; a version that uses fetch_add() with relaxed ordering may be desirable for extra performance at the cost of not-always-up-to-date accumulation results in all threads. As of this writing this can be done by the user by providing a custom type that defines += as explicitly using fetch_add() with relaxed ordering; but we could provide an API for this.

Namespace flow::util
Since Flow gained its first users beyond the original author, some Flow-adjacent code has been written from which Flow can benefit, including additions to flow::util module. (Flow itself continued to be developed, but some features were added elsewhere for expediency; this is a reminder to factor them out into Flow for the benefit of all.) Some features to migrate here might be: conversion between boost.chrono and std.chrono types; (add more here).
Class flow::util::Basic_blob< Allocator, S_SHARING_ALLOWED >
Write a class template, perhaps Tight_blob<Allocator, bool>, which would be identical to Basic_blob but forego the framing features, namely size() and start(), thus storing only the RAII array pointer data() and capacity(); rewrite Basic_blob in terms of this Tight_blob. This simple container type has had some demand in practice, and Basic_blob can and should be cleanly built on top of it (perhaps even as an IS-A subclass).
Member flow::util::Lock_guard_non_recursive
Lock_guard_non_recursive is deprecated, now that C++1x made the more flexible Lock_guard<Mutex_non_recursive> possible. Remove it and all (outside) uses eventually.
Member flow::util::Lock_guard_noop_shared_non_recursive_ex
Lock_guard_noop_shared_non_recursive_ex is deprecated, now that C++1x made the more flexible Lock_guard<Mutex_noop_shared_non_recursive> possible. Remove it and all (outside) uses eventually.
Member flow::util::Lock_guard_noop_shared_non_recursive_sh
Lock_guard_noop_shared_non_recursive_sh is deprecated, now that C++1x made the more flexible Shared_lock_guard<Mutex_noop_shared_non_recursive> possible. Remove it and all (outside) uses eventually.
Member flow::util::Lock_guard_recursive
Lock_guard_recursive is deprecated, now that C++1x made the more flexible Lock_guard<Mutex_recursive> possible. Remove it and all (outside) uses eventually.
Member flow::util::Lock_guard_shared_non_recursive_ex
Lock_guard_shared_non_recursive_ex is deprecated, now that C++1x made the more flexible Lock_guard<Mutex_shared_non_recursive> possible. Remove it and all (outside) uses eventually.
Member flow::util::Lock_guard_shared_non_recursive_sh
Lock_guard_shared_non_recursive_sh is deprecated, now that C++1x made the more flexible Shared_lock_guard<Mutex_shared_non_recursive> possible. Remove it and all (outside) uses eventually.
Member flow::util::Mutex_shared_non_recursive
Consider changing util::Mutex_shared_non_recursive from boost::shared_mutex to std::shared_mutex, as the former has a long-standing unresolved ticket about its impl being slow and/or outdated (as of Boost-1.80). However see also the note on util::Mutex_shared_non_recursive that explains why it might be best to avoid this type of mutex altogether in the first place (in most cases).
Class flow::util::Rnd_gen_uniform_range< range_t >
Rnd_gen_uniform_range and Rnd_gen_uniform_range_mt, on reflection, are very similar to boost::random::random_number_generator, and should be written to conform to it, so as to be usable in std::random_shuffle(), and also to gain its appealing extra features without losing any of the already-available simplicity. Note, however, that std::random_shuffle() is deprecated (and gone in C++17) in favor of std::shuffle(), which is available from C++11 on, and which works directly with a formal URNG such as Rnd_gen_uniform_range_base::Random_generator. So this to-do is less about that and more about gaining those other features while being suitable for random_shuffle(), which some code does still use out there.
Member flow::util::schedule_task_from_now (log::Logger *logger_ptr, const Fine_duration &from_now, bool single_threaded, Task_engine *task_engine, Scheduled_task_handler &&task_body_moved)

We could eliminate schedule_task_from_now() potential limitation versus Timer wherein each call constructs (internally) a new Timer. A pool of Timers can be internally maintained to implement this. This may or may not be worth the complexity, but if the API can remain identically simple while cleanly eliminating the one perf-related reason to choose Timer over this simpler facility, then that is a clean win from the API user's point of view. By comparison, other possible improvements mentioned complicate the API which makes them less attractive.

schedule_task_from_now() and surrounding API provides an easy way to schedule a thing into the future, but it is built on top of boost.asio util::Timer directly; an intermediate wrapper class around this would be quite useful in its own right so that all boost.asio features including its perf-friendliness would be retained along with eliminating its annoyances (around canceling-but-not-really and similar). Then scheduled_task_from_now() would be internally even simpler, while a non-annoying util::Timer would become available for more advanced use cases. echan may have such a class (in a different project) ready to adapt (called Serial_task_timer). I believe it internally uses integer "task ID" to distinguish between scheduled tasks issued in some chronological order, so that boost.asio firing a task after it has been canceled/pre-fired can be easily detected.

Member flow::util::setup_auto_cleanup (const Cleanup_func &func)
setup_auto_cleanup() should take a function via move semantics.
Class flow::util::Shared_ptr_alias_holder< Target_ptr, Const_target_ptr >

Add example usage snippets in Shared_ptr_alias_holder doc header, illustrating the pattern.

flow::util::Shared_ptr_alias_holder Const_target_ptr is deprecated and shall be always left at its default value in future code; eventually remove it entirely and hard-code the default value internally.

Class flow::util::String_ostream
Consider using alt_sstream in boost.format; it doesn't seem to have public documentation but isn't in a detail directory either. So that's interesting. It might have better performance than the implementation here (by being more "direct" probably). Then again it might not.
Member flow::util::Timer

Upgrade to newer Boost and keep testing timer resolutions on the above and other OS versions. Update: As of this writing we are on Boost 1.74 now. Needless to say, it is time to reassess the above, as it has been years since 1.50 – and we had been on 1.63 and 1.66 for long period of time, let alone 1.74. Update: No changes with Boost 1.75, the newest version as of this writing.

Test macOS timer fidelity.

Member flow::util::to_mbit_per_sec (N_items items_per_time, size_t bits_per_item=8)
boost.unit "feels" like it would do this for us in some amazingly pithy and just-as-fast way. Because Boost.
Member FLOW_LOG_WARNING (ARG_stream_fragment)
We can avoid using macros for this and similar APIs by requiring the user to use commas instead of the usual << and by implementing this as a variadic function template (C++11 feature).