Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new ALM transport #89

Closed
wants to merge 106 commits into from
Closed
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
2aad496
Add new type for peer-local timestamps
Neverlord May 18, 2020
52bbdb4
Implement new routing table type
Neverlord May 18, 2020
487e711
Add new type for source routing paths
Neverlord May 18, 2020
073a33f
Implement new ALM layer for peer-to-peer messages
Neverlord May 18, 2020
4f2bf07
Re-integrate mixin unit tests
Neverlord May 18, 2020
7d33e65
Fix path-blacklist flags, add disable-forwarding
Neverlord May 18, 2020
474ea65
Re-integrate peer unit test
Neverlord May 18, 2020
f0c92d6
Implement disable-forwarding flag
Neverlord May 18, 2020
4762d22
Fix handling of configuration parameters
Neverlord May 19, 2020
638ade5
Add initial scaffold for new gateway class
Neverlord May 19, 2020
857d08d
Add domain-specific adaptation to the core actor
Neverlord May 20, 2020
cf3fdb5
Implement forwarding between gateway domains
Neverlord May 21, 2020
f2fea3e
Add gateway::peer, allow using URIs for peerings
Neverlord May 21, 2020
2fdd94a
Add new broker-gateway standalone tool
Neverlord May 21, 2020
f77546b
Add documentation for C++ devs
Neverlord May 31, 2020
78b75c7
Add explicit down message handler to the master
Neverlord May 31, 2020
b06360a
Rename { publisher => entity }_id
Neverlord May 31, 2020
0833890
Add initial scaffold for a new channel abstraction
Neverlord Jun 6, 2020
2efeb26
Implement periodic ACKs and NACK timeouts
Neverlord Jun 9, 2020
aacdfa0
Handle missing events, add messaging scaffold
Neverlord Jun 13, 2020
e94edde
Detect potentially lost messages at the producer
Neverlord Jun 13, 2020
fa6cb79
Switch to a heartbeat-based design for producers
Neverlord Jun 14, 2020
248538f
Implement consumer timeouts
Neverlord Jun 18, 2020
d21574b
Re-implement master and clone actors with channels
Neverlord Jun 27, 2020
93ea0ad
Fix meta data reading and writing
Neverlord Jun 28, 2020
5b6886c
Fix compiler errors and warnings on GCC 8
Neverlord Jun 28, 2020
71a4cca
Add await_peer to allow race-free testing
Neverlord Jun 29, 2020
fa52d92
Re-implement proper signaling for put_unique
Neverlord Jun 30, 2020
90d92ca
Fix initialization of channels and Python tests
Neverlord Jul 7, 2020
d226254
Document intra-store communication
Neverlord Jul 12, 2020
59c17eb
Add asynchronous await functions
Neverlord Jul 12, 2020
423743f
Fix build on MSVC
Neverlord Jul 12, 2020
ac6fecd
Consistenly use disable-forwarding as flag name
Neverlord Jul 12, 2020
513f0f6
Add documentation for the new broker-gateway tool
Neverlord Jul 12, 2020
67041af
Add documentation for await_peer and await_idle
Neverlord Jul 15, 2020
235db00
Fix build on MSVC
Neverlord Jul 15, 2020
5512721
Use Debian 9 for the memcheck build
Neverlord Jul 15, 2020
dfe8c0c
Integrate review feedback on dev section
Neverlord Jul 25, 2020
00afa30
Clean up remaining TODO in Publishing Data Section
Neverlord Jul 25, 2020
6ea63ae
Merge branch 'master'
Neverlord Jul 30, 2020
48c1326
Merge branch 'topic/neverlord/caf-0.18'
Neverlord Jul 30, 2020
d781a31
Fix ALM branch build with CAF 0.18
Neverlord Jul 31, 2020
8a7a328
Disable integration tests with old file format
Neverlord Jul 31, 2020
641da73
Port Broker to CAF 0.18.rc.1
Neverlord Sep 25, 2020
800805a
Fix backwards compatibility with CAF 0.17 series
Neverlord Sep 26, 2020
9e3d5b4
Re-implement automatic reconnects to lost peers
Neverlord Sep 27, 2020
df7172a
Update tests to latest semantics and API changes
Neverlord Sep 28, 2020
2b094b1
Allow store objects to outlive the Broker endpoint
Neverlord Sep 29, 2020
8bdcb0e
Fix deadlock in flare_actor::dequeue
Neverlord Sep 30, 2020
372e4b6
Break infinite loop in store proxies
Neverlord Sep 30, 2020
2eb63fc
Fix response messages to put_unique requests
Neverlord Sep 30, 2020
8745857
Revert reconnect behavior to v1.3.3 state
Neverlord Sep 30, 2020
f1916db
Fix build with CAF 0.18-rc.1
Neverlord Sep 30, 2020
421fff2
Add new system testing suite
Neverlord Oct 10, 2020
3acbd0c
Implement graceful shutdown of ALM peers
Neverlord Oct 16, 2020
bf1e2bb
Remove extra semicolons
Neverlord Oct 16, 2020
fe358da
Fix segfault during shutdown
Neverlord Oct 19, 2020
022d817
Make peering handshake more robust
Neverlord Oct 26, 2020
55464ee
Factor out handshake logic into an FSM
Neverlord Oct 28, 2020
1972b93
Port stream_transport to new handshake FSM
Neverlord Oct 28, 2020
bcae433
Fix build with CAF 0.17
Neverlord Oct 29, 2020
69124b1
Always perform the ping-pong step when peering
Neverlord Oct 31, 2020
a1122c5
Fix race condition in SSL test suite
Neverlord Oct 31, 2020
5d49304
Fix build on GCC
Neverlord Oct 31, 2020
c67e3f0
Merge branch 'master'
Neverlord Oct 31, 2020
cda0524
Remove obsolete includes
Neverlord Oct 31, 2020
91a9529
Restore timespan/timestamp format for Zeek
Neverlord Oct 31, 2020
ce9d3a9
Add missing to_string for status_view
Neverlord Nov 8, 2020
7fa1efc
Enable BROKER_ASSERT for debug builds
Neverlord Nov 8, 2020
d758fed
Check timestamp before updating peer subscriptions
Neverlord Nov 8, 2020
60a6a70
Fix handling of initial subscription set
Neverlord Nov 8, 2020
8a8869b
Add to_string overload for endpoint_info
Neverlord Nov 8, 2020
fad532c
Suppress automatic reconnects at the responder
Neverlord Nov 14, 2020
acfade5
Add additional logging
Neverlord Nov 14, 2020
103f33e
Add missing to_string overloads
Neverlord Nov 15, 2020
c784e21
Remove dead code
Neverlord Nov 21, 2020
e233b22
Split unit test suite for the alm::peer
Neverlord Nov 21, 2020
fbf02f0
Fix output of configuration::dump_content
Neverlord Nov 21, 2020
5d38157
Clean up todo entries and dead code
Neverlord Nov 21, 2020
5450517
Fix build with BROKER_ASSERT enabled
Neverlord Nov 21, 2020
2f0c902
Reduce compilation complexity in peer unit tests
Neverlord Nov 21, 2020
82f96a2
Grant container builds more memory
Neverlord Nov 22, 2020
e18628c
De-templatize alm::peer and alm::stream_transport
Neverlord Nov 29, 2020
9c3c2a8
Update developer guide
Neverlord Nov 29, 2020
753423a
De-templatize make_behavior functions
Neverlord Nov 30, 2020
059cc4f
Merge branch 'master'
Neverlord Feb 23, 2021
6cbf4cf
Prefix actor names with 'broker.'
Neverlord Mar 14, 2021
a4cb170
Add micro benchmarks for routing table algorithms
Neverlord Mar 15, 2021
6623073
Use more efficient data structures for ALM layer
Neverlord Mar 18, 2021
6d71951
Remove unnecessary include
Neverlord Apr 17, 2021
94de3cd
Add new micro benchmark for serialization
Neverlord Apr 17, 2021
5c9b026
Add new micro benchmark for streaming
Neverlord Apr 18, 2021
6eff279
Remove dead code / comments
Neverlord Apr 19, 2021
84b7361
Fix integration test suite with latest CAF version
Neverlord Apr 19, 2021
092186e
Encode receivers directly in the multipath object
Neverlord Apr 19, 2021
6dd538d
Extend micro benchmark suites
Neverlord Apr 19, 2021
5d8039c
Use the term "revocation" consistently
Neverlord Apr 19, 2021
db3f9cb
Reduce redundancy in type ID setup
Neverlord Apr 20, 2021
0919dd6
Add benchmarks for alternative routing tables
Neverlord Apr 20, 2021
51ed23d
Fix dispatching of command messages
Neverlord Apr 20, 2021
67ee323
Fix equality operator for alm::multipath
Neverlord Apr 21, 2021
32f9ab2
Pack node messages more efficiently on the wire
Neverlord Apr 21, 2021
f486e31
Remove extra semicolon
Neverlord Apr 22, 2021
c2e1ac8
Fix deprecation warnings
Neverlord Apr 22, 2021
fb945ce
Fix build on GCC
Neverlord Apr 22, 2021
9fcb3e4
Update CAF submodule
Neverlord Apr 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ freebsd_task:
memcheck_task:
container:
# Just uses a recent/common distro to run memory error/leak checks.
dockerfile: ci/ubuntu-18.04/Dockerfile
dockerfile: ci/debian-9/Dockerfile
cpu: 8
# AddressSanitizer uses more memory than normal config.
memory: 8GB
Expand Down
7 changes: 5 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -303,17 +303,19 @@ set(BROKER_SRC
src/detail/prefix_matcher.cc
src/detail/sqlite_backend.cc
src/detail/store_actor.cc
src/domain_options.cc
src/endpoint.cc
src/endpoint_info.cc
src/entity_id.cc
src/error.cc
src/filter_type.cc
src/gateway.cc
src/internal_command.cc
src/mailbox.cc
src/network_info.cc
src/peer_status.cc
src/port.cc
src/publisher.cc
src/publisher_id.cc
src/status.cc
src/status_subscriber.cc
src/store.cc
Expand Down Expand Up @@ -360,8 +362,9 @@ macro(add_tool name)
endmacro()

if (NOT BROKER_DISABLE_TOOLS)
add_tool(broker-pipe)
add_tool(broker-gateway)
add_tool(broker-node)
add_tool(broker-pipe)
endif ()

# -- Bindings -----------------------------------------------------------------
Expand Down
32 changes: 27 additions & 5 deletions bindings/python/_broker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ PYBIND11_MAKE_OPAQUE(broker::set)
PYBIND11_MAKE_OPAQUE(broker::table)
PYBIND11_MAKE_OPAQUE(broker::vector)

namespace {

broker::endpoint_id node_from_str(const std::string& node_str) {
caf::node_id node;
if (auto err = caf::parse(node_str, node))
throw std::invalid_argument(
"endpoint::await_peer called with invalid endpoint ID");
return node;
};

} // namespace

PYBIND11_MODULE(_broker, m) {
m.doc() = "Broker python bindings";
py::module mb = m.def_submodule("zeek", "Zeek-specific bindings");
Expand Down Expand Up @@ -124,7 +136,8 @@ PYBIND11_MODULE(_broker, m) {
.def("drop_all_on_destruction", &broker::publisher::drop_all_on_destruction)
.def("publish", (void (broker::publisher::*)(broker::data d)) &broker::publisher::publish)
.def("publish_batch",
[](broker::publisher& p, std::vector<broker::data> xs) { p.publish(xs); });
[](broker::publisher& p, std::vector<broker::data> xs) { p.publish(xs); })
.def("reset", &broker::publisher::reset);

using subscriber_base = broker::subscriber_base<broker::subscriber::value_type>;
using topic_data_pair = std::pair<broker::topic, broker::data>;
Expand Down Expand Up @@ -190,7 +203,8 @@ PYBIND11_MODULE(_broker, m) {

py::class_<broker::subscriber, subscriber_base>(m, "Subscriber")
.def("add_topic", &broker::subscriber::add_topic)
.def("remove_topic", &broker::subscriber::remove_topic);
.def("remove_topic", &broker::subscriber::remove_topic)
.def("reset", &broker::subscriber::reset);

py::bind_vector<std::vector<broker::status_subscriber::value_type>>(m, "VectorStatusSubscriberValueType");

Expand Down Expand Up @@ -222,7 +236,8 @@ PYBIND11_MODULE(_broker, m) {
[](broker::status_subscriber& ep) -> std::vector<broker::status_subscriber::value_type> {
return ep.poll(); })
.def("available", &broker::status_subscriber::available)
.def("fd", &broker::status_subscriber::fd);
.def("fd", &broker::status_subscriber::fd)
.def("reset", &broker::status_subscriber::reset);

py::class_<broker::status_subscriber::value_type>(status_subscriber, "ValueType")
.def("is_error",
Expand All @@ -239,8 +254,7 @@ PYBIND11_MODULE(_broker, m) {
py::class_<broker::broker_options>(m, "BrokerOptions")
.def(py::init<>())
.def_readwrite("disable_ssl", &broker::broker_options::disable_ssl)
.def_readwrite("ttl", &broker::broker_options::ttl)
.def_readwrite("forward", &broker::broker_options::forward)
.def_readwrite("disable_forwarding", &broker::broker_options::disable_forwarding)
.def_readwrite("ignore_broker_conf", &broker::broker_options::ignore_broker_conf)
.def_readwrite("use_real_time", &broker::broker_options::use_real_time);

Expand Down Expand Up @@ -324,6 +338,14 @@ PYBIND11_MODULE(_broker, m) {
[](broker::endpoint& ep, const std::string& name) -> broker::expected<broker::store> {
return ep.attach_clone(name);
})
.def("await_peer",
[](broker::endpoint& ep, const std::string& node_str) {
return ep.await_peer(node_from_str(node_str));
})
.def("await_peer",
[](broker::endpoint& ep, const std::string& node_str, broker::timespan timeout) {
return ep.await_peer(node_from_str(node_str), timeout);
})
;
}

122 changes: 92 additions & 30 deletions bindings/python/broker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ class Subscriber:
def __init__(self, internal_subscriber):
self._subscriber = internal_subscriber

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
self._subscriber.reset()
self._subscriber = None

def get(self, *args, **kwargs):
msg = self._subscriber.get(*args, **kwargs)

Expand Down Expand Up @@ -161,6 +168,13 @@ class StatusSubscriber(_broker.Subscriber):
def __init__(self, internal_subscriber):
self._subscriber = internal_subscriber

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
self._subscriber.reset()
self._subscriber = None

def get(self, *args, **kwargs):
x = self._subscriber.get(*args, **kwargs)
return self._to_result(x)
Expand Down Expand Up @@ -198,6 +212,13 @@ class Publisher:
def __init__(self, internal_publisher):
self._publisher = internal_publisher

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
self._publisher.reset()
self._publisher = None

def demand(self):
return self._publisher.demand()

Expand Down Expand Up @@ -226,12 +247,20 @@ def publish_batch(self, *batch):

class Store:
# This class does not derive from the internal class because we
# need to pass in existign instances. That means we need to
# need to pass in existing instances. That means we need to
# wrap all methods, even those that just reuse the internal
# implementation.
def __init__(self, internal_store):
self._store = internal_store

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
self._store.reset()
self._parent = None
self._store = None

def name(self):
return self._store.name()

Expand Down Expand Up @@ -328,6 +357,16 @@ def pop(self, key, expiry=None):
def _to_expiry(self, e):
return (_broker.OptionalTimespan(_broker.Timespan(float(e))) if e is not None else _broker.OptionalTimespan())

def await_idle(self, timeout=None):
if timeout:
return self._store.await_idle(_broker.Timespan(float(timeout)))
else:
return self._store.await_idle()

# Points to the "owning" Endpoint to make sure Python cleans this object up
# before destroying the endpoint.
_parent = None

class Endpoint(_broker.Endpoint):
def make_subscriber(self, topics, qsize = 20):
topics = _make_topics(topics)
Expand Down Expand Up @@ -360,13 +399,36 @@ def attach_master(self, name, type=None, opts={}):
bopts = _broker.MapBackendOptions() # Generator expression doesn't work here.
for (k, v) in opts.items():
bopts[k] = Data.from_py(v)

s = _broker.Endpoint.attach_master(self, name, type, bopts)
return Store(s.get()) if s.is_valid() else None
if not s.is_valid():
return None
result = Store(s.get())
# This extra reference establishes a parent-child relation between the
# two Python objects, making sure that the garbage collector destroys
# the store *before* cleaning up the endpoint
result._parent = self
return result

def attach_clone(self, name):
s = _broker.Endpoint.attach_clone(self, name)
return Store(s.get()) if s.is_valid() else None
if not s.is_valid():
return None
result = Store(s.get())
# Same as above: make sure Python cleans up the store first.
result._parent = self
return result

def await_peer(self, node, timeout=None):
if timeout:
return _broker.Endpoint.await_peer(self, node, _broker.Timespan(float(timeout)))
else:
return _broker.Endpoint.await_peer(self, node)

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
self.shutdown()

class Message:
def to_broker(self):
Expand Down Expand Up @@ -505,56 +567,56 @@ def _try_bytes_decode(b):
# class Store:
# def __init__(self, handle):
# self.store = handle
#
#
# def name(self):
# return self.store.name()
#
#
# class Mailbox:
# def __init__(self, handle):
# self.mailbox = handle
#
#
# def descriptor(self):
# return self.mailbox.descriptor()
#
#
# def empty(self):
# return self.mailbox.empty()
#
#
# def count(self, n = -1):
# return self.mailbox.count(n)
#
#
#
#
# class Message:
# def __init__(self, handle):
# self.message = handle
#
#
# def topic(self):
# return self.message.topic().string()
#
#
# def data(self):
# return self.message.data() # TODO: unwrap properly
#
#
# def __str__(self):
# return "%s -> %s" % (self.topic(), str(self.data()))
#
#
#
#
# class BlockingEndpoint(Endpoint):
# def __init__(self, handle):
# super(BlockingEndpoint, self).__init__(handle)
#
#
# def subscribe(self, topic):
# self.endpoint.subscribe(topic)
#
#
# def unsubscribe(self, topic):
# self.endpoint.unsubscribe(topic)
#
#
# def receive(self, x):
# if x == Status:
# return self.endpoint.receive()
# elif x == Message:
# return Message(self.endpoint.receive())
# else:
# raise BrokerError("invalid receive type")
#
#
# #def receive(self):
# # if fun1 is None:
# # return Message(self.endpoint.receive())
Expand All @@ -565,34 +627,34 @@ def _try_bytes_decode(b):
# # return self.endpoint.receive_msg(fun1)
# # raise BrokerError("invalid receive callback arity; must be 1 or 2")
# # return self.endpoint.receive_msg_or_status(fun1, fun2)
#
#
# def mailbox(self):
# return Mailbox(self.endpoint.mailbox())
#
#
#
#
# class NonblockingEndpoint(Endpoint):
# def __init__(self, handle):
# super(NonblockingEndpoint, self).__init__(handle)
#
#
# def subscribe(self, topic, fun):
# self.endpoint.subscribe_msg(topic, fun)
#
#
# def on_status(fun):
# self.endpoint.subscribe_status(fun)
#
#
# def unsubscribe(self, topic):
# self.endpoint.unsubscribe(topic)
#
#
#
#
# class Context:
# def __init__(self):
# self.context = _broker.Context()
#
#
# def spawn(self, api):
# if api == Blocking:
# return BlockingEndpoint(self.context.spawn_blocking())
# elif api == Nonblocking:
# return NonblockingEndpoint(self.context.spawn_nonblocking())
# else:
# raise BrokerError("invalid API flag: " + str(api))
#
#
9 changes: 6 additions & 3 deletions bindings/python/store.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,14 @@ void init_store(py::module& m) {
.def("increment", &broker::store::increment)
.def("decrement", &broker::store::decrement)
.def("append", &broker::store::append)
.def("insert_into", (void (broker::store::*)(broker::data, broker::data, broker::optional<broker::timespan>) const) &broker::store::insert_into)
.def("insert_into", (void (broker::store::*)(broker::data, broker::data, broker::data, broker::optional<broker::timespan>) const) &broker::store::insert_into)
.def("insert_into", (void (broker::store::*)(broker::data, broker::data, broker::optional<broker::timespan>)) &broker::store::insert_into)
.def("insert_into", (void (broker::store::*)(broker::data, broker::data, broker::data, broker::optional<broker::timespan>)) &broker::store::insert_into)
.def("remove_from", &broker::store::remove_from)
.def("push", &broker::store::push)
.def("pop", &broker::store::pop);
.def("pop", &broker::store::pop)
.def("await_idle", [](broker::store& st) { return st.await_idle(); })
.def("await_idle", [](broker::store& st, broker::timespan timeout) { return st.await_idle(timeout); })
.def("reset", &broker::store::reset);

// Don't need.
// py::class_<broker::store::response>(store, "Response")
Expand Down
Binary file added doc/_images/core-actor-uml.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/_images/endpoint.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions doc/_images/endpoint.xml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions doc/_images/gateway.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mxfile modified="2020-07-12T13:25:38.874Z" host="Electron" agent="5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.3.9 Chrome/83.0.4103.119 Electron/9.0.5 Safari/537.36" etag="LaupmYtKdVszVeVFEYXr" version="13.3.9" type="device"><diagram id="l8hXMBHkgcEJcSW0mbfh" name="Page-1">7Vpbc5s4FP41fqwHEGB4bBI7bbY7m2l6ye5LRwEZtMHII+TY7q+vZCRuAsdtcEw9fTI6knX5vqPvII5G4HKxuaZwGf9NQpSMLCPcjMDVyLJM03D5j7BspcUwQG6JKA6lrTTc4e9INZTWFQ5RVmvICEkYXtaNAUlTFLCaDVJK1vVmc5LUR13CCGmGuwAmuvUrDlmsFub6ZcU7hKNYDu1Zk7xiAVVjuZIshiFZV0xgOgKXlBCWPy02lygR6Clc8v/NOmqLiVGUskP+YN8sP4ZfP7ro8T68RgT67P6/N0B28wSTlVzxdMMQTWEiZ822CgreHUedFy74UpbCGCRkxXu/WMeYobslDIRxzT2B22K2SHjJ5I8RhSHms7wkCaG7vsBs5vsA8Lo5ThJlT0kqep/zYd7Jvz4hyjBn422Co5TbGBF9Q1lK0JyJP5CU3cl5Gqqce5Lp8rKOlFo17x1tKiaJHIdngRjd8iaq1pYsSj9WpK5LnwDKFlfcwfKkEUo/jIquS6r4g2TrZ5gDGnPv0xMzp3ihOQAd7L0CXY4xNLomGlt7WHqGFZgtc6mb4w0KG1RwikKIvHkg2GaUPKJKjRt46GHeD8Yc5BrGxRapgGy3YGwfC2JfRzTkWi6L0kUpWaWhAG3nehVQ0Qaze2kWz/+K57EjSimf3b1qJgrVuvCtiDLlANwyw2Liu57+R4xtpXPDFSPcRCiLSUT4Pv1AxH4oRr/aVIa/2qqCGLCs2pVUXSeLGVnRAO3BylSxFNIIsT0NLRnABZR7vYKiBDL8VA+bvXOsXgt6JNnskcZXIGQyLD7094ffXddAI3Z4J5Y10zo7iK0GxKZ/aozBkWSlNXL8xnFjcqBKKacdikzZZ7eHgDm0PeRokD67YczKhjGqG8YYHe1V6wUx+lDvV0eLoXi/24u6mV3qZhxL3V6BKtMbFlXe2QmVaw9NqPo5KNa0ayAKpYTneYVyBuX2at5n7Pan9vo2RI/3ijuU/WAfGgbsYe2H/s8jxhmRZQ6LLOfsxGtiDUy83LODuPkN5OQQ+wdAXNGgcvNPS2tDkloFokVIqtL1jAzlnKhUrNXG0mzmGcbLPnNYBx8fhvU5FhzyHvWHxI7AMxQS9Y9V15ChNdxqXHKtYXXo60CqjHp3qjZAImk80nO1CxyGSZea1j2kuNmwKzwiFsSjo6V27eZx0tCF02sRTutox0k9tfv+pMFJJe2vMOVdYSLYXKOMjfbn83vgxnEHlhK29K00HRw3CL4GN2Bw3BxwJYKsWIJTjqS63CVEJYRZXGiPkq0P8AEltyTDEtQHwhhZtOha/QZRIX9VCZV3ZBabSFxqG8N1BsbBKuP9IfotklKssT9zPIdLt06kVNxulTyGKrbceGlTxT7uu9CVsf7sgpt/bi4+LfCt8SX8y35jalz+1AG218NoI64fOfNlv/DdQf71lmA+xfK44Dtjz5l4rg0mruV6vt3YzfbYtC0TOK4FJqC4d6kGyJcn+yyp1YdpXuqZNBwkX7/W0c5HitX+utvYL3QblZEoL+70nnHdk1ctvrvkw1c/k3XmaXt0UXVKPjA727uPAmO/jzo9+WgzkjVF7Jd9lBfL+7h58/JaM5j+AA==</diagram></mxfile>
Binary file added doc/_images/gateway.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading