From dd63adf1dd0ce86a1f91c9e87b5a0d85d160b15b Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Mon, 6 Jul 2015 17:40:33 +0900 Subject: [PATCH] Add initial deployment guide with unit configurations and what should be a correct topo graph. Nginx configuration is still missing. --- .gitignore | 1 - README.md | 4 + doc/DEPLOYMENT.md | 529 ++++++++++++++++++++++++++++++++++++++++++++++ doc/topo-v1.ditaa | 125 +++++------ doc/topo-v1.png | Bin 0 -> 32820 bytes 5 files changed, 596 insertions(+), 63 deletions(-) create mode 100644 doc/DEPLOYMENT.md create mode 100644 doc/topo-v1.png diff --git a/.gitignore b/.gitignore index 1287ffea..f8449981 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ .idea/ /*.tgz /.env -/doc/*.png /node_modules/ /res/bower_components/ /res/build/ diff --git a/README.md b/README.md index 2ccb712e..78eee0a8 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,10 @@ To update your development version, simply pull the repo and run `npm install` a ## FAQ +### Can I deploy STF to actual servers? + +Yes, see [DEPLOYMENT](doc/DEPLOYMENT.md). + ### Will I have to change battery packs all the time? Nope, we've had many devices running since the initial prototype phase about two years ago, and we've only had a single incident so far. The battery expanded causing the casing to split from the seams. The device itself was working fine and reporting full battery health, but it was discarded due to safety reasons. diff --git a/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md new file mode 100644 index 00000000..f8e6803e --- /dev/null +++ b/doc/DEPLOYMENT.md @@ -0,0 +1,529 @@ +# Deployment + +So you've got STF running via `stf local` and now you'd like to deploy it to a real server. While there are of course various ways to set everything up, this document will focus on a [systemd](http://www.freedesktop.org/wiki/Software/systemd/) + [Docker](https://www.docker.com/) deployment. + +STF consists of multiple independent processes communicating via [ZeroMQ](http://zeromq.org/) and [Protocol Buffers](https://github.com/google/protobuf). We call each process a "unit" to match systemd terminology. + +The core topology is as follows. + +![Rough core topology](topo-v1.png) + +## Assumptions + +For this example deployment, the following assumptions will be made. You will need to adjust them as you see fit. Note that this deployment was designed to be relatively easy to set up without external tools, and may not be optimal. They're also configured so that you can run everything on a single host if required. + +* You have [systemd](http://www.freedesktop.org/wiki/Software/systemd/) running on each host +* You have [Docker](https://www.docker.com/) running on each host +* Each host has an `/etc/environment` (a la [CoreOS](https://coreos.com/)) file with `COREOS_PRIVATE_IPV4=MACHINE_IP_HERE`. This is used to load the machine IP address in configuration files. + - You can create the file yourself or alternatively replace `${COREOS_PRIVATE_IPV4}` manually as required. +* You're deploying [openstf/stf](https://registry.hub.docker.com/u/openstf/stf/):1.0.0. Adjust if another version required. +* You want to access the app at https://stf.example.org/. Change to the actual URL you want to use. +* You have RethinkDB running on `rethinkdb.stf.example.org`. Change to the actual address/IP where required. + - You may also use SRV records by giving the url in `srv+tcp://rethinkdb-28015.skydns.stf.example.org` format. +* You have two static IPs available for the main communication bridges (or "triproxies"), or are able to figure out an alternate method. In this example we'll use `devside.stf.example.org` and `appside.stf.example.org` as memorable addresses. + - You can also use SRV records as mentioned above. + +## Roles + +Since we're dealing with actual physical devices, some units need to be deployed to specific servers to make sure that they actually connect with the devices. We currently use [fleet](https://github.com/coreos/fleet), but in this example deployment we'll just assume that you already know how you wish to deploy and distribute the systemd units. + +### Provider role + +The provider role requires the following units, which must be together on a single or more hosts. + +* [adbd.service](#adbservice) +* [stf-provider@.service](#stf-providerservice) + +### App role + +The app role can contain any of the following units. You may distribute them as you wish, as long as the [assumptions above](#assumptions) hold. Some units may have more requirements, they will be listed where applicable. + +* [rethinkdb-proxy-28015.service](#rethinkdb-proxy-28015service) +* [stf-app@.service](stf-appservice) +* [stf-auth@.service](stf-authservice) +* [stf-migrate.service](stf-migrateservice) +* [stf-processor@.service](stf-processorservice) +* [stf-provider@.service](stf-providerservice) +* [stf-reaper.service](stf-reaperservice) +* [stf-storage-plugin-apk@.service](stf-storage-plugin-apkservice) +* [stf-storage-plugin-image@.service](stf-storage-plugin-imageservice) +* [stf-storage-temp@.service](stf-storage-tempservice) +* [stf-triproxy-app.service](stf-triproxy-appservice) +* [stf-triproxy-dev.service](stf-triproxy-devservice) +* [stf-websocket@.service](stf-websocketservice) +* [stf-notify-hipchat.service](stf-notify-hipchatservice) + +## Support units + +These external units are required for the actual STF units to work. + +### adbd.service + +You need to have a single `adbd.service` unit running on each host where you have devices connected. + +The docker container comes with a default, insecure ADB key for convenience purposes, so that you won't have to accept a new ADB key on your devices each time the unit restarts. This is insecure because anyone in possession of the insecure key will then be able to access your device without any prompt, assuming they have physical access to it. This may or may not be a problem for you. See [sorccu/adb](https://registry.hub.docker.com/u/sorccu/adb/) for more information if you'd like to provide your own keys. + +```ini +[Unit] +Description=ADB daemon +After=docker.service +Requires=docker.service + +[Service] +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull sorccu/adb:latest +ExecStartPre=-/usr/bin/docker kill %p +ExecStartPre=-/usr/bin/docker rm %p +ExecStart=/usr/bin/docker run --rm \ + --name %p \ + --privileged \ + -v /dev/bus/usb:/dev/bus/usb \ + --net host \ + sorccu/adb:latest +ExecStop=-/usr/bin/docker stop -t 2 %p +``` + +### rethinkdb-proxy-28015.service + +You need a single instance of the `rethinkdb-proxy-28015.service` unit on each host where you have another unit that needs to access the database. Having a local proxy simplifies configuration for other units and allows the `AUTHKEY` to be specified only once. + +```ini +[Unit] +Description=RethinkDB proxy/28015 +After=docker.service +Requires=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull ctlc/ambassador:latest +ExecStartPre=-/usr/bin/docker kill %p +ExecStartPre=-/usr/bin/docker rm %p +ExecStart=/usr/bin/docker run --rm \ + --name %p \ + -e "AUTHKEY=YOUR_RETHINKDB_AUTH_KEY_HERE_IF_ANY" \ + -p 28015 \ + -e RETHINKDB_PORT_28015_TCP=tcp://rethinkdb.stf.example.org:28015 \ + ctlc/ambassador:latest +ExecStop=-/usr/bin/docker stop -t 10 %p +``` + +## Main units + +These units are required for proper operation of STF. Unless mentioned otherwise, each unit can have multiple running instances (possibly on separate hosts) if desired. + +### stf-app@.service + +**Requires** the `rethinkdb-proxy-28015.service` unit on the same host. + +The app unit provides the main HTTP server and currently a very, very modest API for the client-side. It also serves all static resources including images, scripts and stylesheets. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-app@3100.service` runs on port 3100). You can have multiple instances running on the same host by using different ports. + +```ini +[Unit] +Description=STF app +After=rethinkdb-proxy-28015.service +BindsTo=rethinkdb-proxy-28015.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + --link rethinkdb-proxy-28015:rethinkdb \ + -e "SECRET=YOUR_SESSION_SECRET_HERE" \ + -p 127.0.0.1:%i:3000 \ + openstf/stf:1.0.0 \ + stf app --port 3000 \ + --auth-url https://stf.example.org/auth/oauth2/ \ + --websocket-url https://stf.example.org/ +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + +### stf-auth@.service + +You have multiple options here. STF currently provides authentication units for [OAuth 2.0](http://oauth.net/2/) and [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol), plus a mock implementation that simply asks for a name and an email address. + +Since the other providers require quite a bit of configuration, we'll simply set up a mock auth unit here. If you'd rather use the real providers, see `stf auth-oauth2 --help` and `stf auth-ldap --help` for the required variables. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-auth@3200.service` runs on port 3200). You can have multiple instances running on the same host by using different ports. + +```ini +[Unit] +Description=STF auth +After=docker.service +Requires=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + -e "SECRET=YOUR_SESSION_SECRET_HERE" \ + -p 127.0.0.1:%i:3000 \ + openstf/stf:1.0.0 \ + stf auth-mock --port 3000 \ + --app-url https://stf.example.org/ +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + +### stf-migrate.service + +**Requires** the `rethinkdb-proxy-28015.service` unit on the same host. + +This unit migrates the database to the latest version, which pretty much means creating tables and setting up indexes. Schema changes do not require a migration unless a new index is introduced. + +This is a oneshot unit, meaning that it shuts down after it's done. + +```ini +[Unit] +Description=STF migrate +After=rethinkdb-proxy-28015.service +BindsTo=rethinkdb-proxy-28015.service + +[Service] +EnvironmentFile=/etc/environment +Type=oneshot +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p +ExecStartPre=-/usr/bin/docker rm %p +ExecStart=/usr/bin/docker run --rm \ + --name %p \ + --link rethinkdb-proxy-28015:rethinkdb \ + openstf/stf:1.0.0 \ + stf migrate +``` + +### stf-processor@.service + +**Requires** the `rethinkdb-proxy-28015.service` unit on the same host. + +The processor is the main workhorse of STF. It acts as a bridge between the devices and the app, and nearly all communication goes through it. You may wish to have more than one instance running. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example the identifier has no special purpose, but having it allows you to start more than one unit on the same host. + +```ini +[Unit] +Description=STF processor +After=rethinkdb-proxy-28015.service +BindsTo=rethinkdb-proxy-28015.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + --link rethinkdb-proxy-28015:rethinkdb \ + openstf/stf:1.0.0 \ + stf processor %p-%i \ + --connect-app-dealer tcp://appside.stf.example.org:7160 \ + --connect-dev-dealer tcp://devside.stf.example.org:7260 +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + +### stf-provider@.service + +**Requires** the `adbd.service` unit on the same host. + +The provider unit connects to ADB and start worker processes for each device. It then sends and receives commands from the processor. + +The name of the provider shows up in the device list, making it easier to see where the physical devices are located. In this configuration the name is set to the hostname. + +Note that the provider needs to be able to manage a certain port range, so `--net host` is required until Docker makes it easier to work with ranges. The ports are used for internal services and the screen capturing WebSocket. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the provider ID, which can then be matched against in the [nginx](http://nginx.org/) configuration later on. The ID should be unique and persistent. This is only one way to set things up, you may choose to do things differently if it seems sketchy. + +Note that you cannot have more than one provider unit running on the same host, as they would compete over which one gets to control the devices. In the future we might add a negotiation protocol to allow for relatively seamless upgrades. + +```ini +[Unit] +Description=STF provider +After=adbd.service +BindsTo=adbd.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + --net host \ + openstf/stf:1.0.0 \ + stf provider \ + --name "%H/%i" \ + --connect-sub tcp://devside.stf.example.org:7250 \ + --connect-push tcp://devside.stf.example.org:7270 \ + --storage-url https://stf.example.org/ \ + --public-ip ${COREOS_PRIVATE_IPV4} \ + --min-port=15000 \ + --max-port=25000 \ + --heartbeat-interval 10000 \ + --screen-ws-url-pattern "wss://stf.example.org/d/%i/<%= serial %>/<%= publicPort %>/" +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + +### stf-reaper.service + +**Requires** the `rethinkdb-proxy-28015.service` unit on the same host. + +The reaper unit receives heartbeat events from device workers, and marks lost devices as absent until a heartbeat is received again. The purpose of this unit is to ensure the integrity of the present/absent flag in the database, in case a provider shuts down unexpectedly or another unexpected failure occurs. It loads the current state from the database on startup and keeps keeps patching its internal view as events are routed to it. + +Note that it doesn't make sense to have more than one reaper running at once, as they would just duplicate the events. + +```ini +[Unit] +Description=STF reaper +After=rethinkdb-proxy-28015.service +BindsTo=rethinkdb-proxy-28015.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p +ExecStartPre=-/usr/bin/docker rm %p +ExecStart=/usr/bin/docker run --rm \ + --name %p \ + --link rethinkdb-proxy-28015:rethinkdb \ + openstf/stf:1.0.0 \ + stf reaper dev \ + --connect-push tcp://devside.stf.example.org:7270 \ + --connect-sub tcp://appside.stf.example.org:7150 \ + --heartbeat-timeout 30000 +ExecStop=-/usr/bin/docker stop -t 10 %p +``` + +### stf-storage-plugin-apk@.service + +The APK storage plugin loads raw blobs from the main storage unit and allows additional actions to be performed on APK files, such as retrieving the `AndroidManifest.xml`. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-storate-plugin-apk@3300.service` runs on port 3300). You can have multiple instances running on the same host by using different ports. + +```ini +[Unit] +Description=STF APK storage plugin +After=docker.service +Requires=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + -p 127.0.0.1:%i:3000 \ + openstf/stf:1.0.0 \ + stf storage-plugin-apk --port 3000 \ + --storage-url https://stf.example.org/ +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + +### stf-storage-plugin-image@.service + +The image storage plugin loads raw blobs from the main storage unit and and allows images to be resized using parameters. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-storage-plugin-image@3400.service` runs on port 3400). You can have multiple instances running on the same host by using different ports. + +```ini +[Unit] +Description=STF image storage plugin +After=docker.service +Requires=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + -p 127.0.0.1:%i:3000 \ + openstf/stf:1.0.0 \ + stf storage-plugin-image --port 3000 \ + --storage-url https://stf.example.org/ +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + +### stf-storage-temp@.service + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-storage-temp@3500.service` runs on port 3500). Currently, ** you cannot have more than one instance of this unit**, as both temporary files and an in-memory mapping is used. Using a template unit makes it easy to set the port. + +```ini +[Unit] +Description=STF temp storage +After=docker.service +Requires=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + -v /mnt/storage:/data \ + -p 127.0.0.1:%i:3000 \ + openstf/stf:1.0.0 \ + stf storage-temp --port 3000 \ + --save-dir /data +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + +### stf-triproxy-app.service + +This unit provides the `appside.stf.example.org` service mentioned earlier. Its purpose is to send and receive requests from the app units, and distribute them across the processor units. It's "dumb" in that it contains no real logic, and you rarely if ever need to upgrade the unit. + +We call it a triproxy because it deals with three endpoints instead of the usual two. + +You may have more than one instance running simultaneously, and then give a comma separated list to the provider. For simplicity we're using a normal unit here. + +```ini +[Unit] +Description=STF app triproxy +After=docker.service +Requires=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p +ExecStartPre=-/usr/bin/docker rm %p +ExecStart=/usr/bin/docker run --rm \ + --name %p \ + --net host \ + openstf/stf:1.0.0 \ + stf triproxy app \ + --bind-pub "tcp://*:7150" \ + --bind-dealer "tcp://*:7160" \ + --bind-pull "tcp://*:7170" +ExecStop=-/usr/bin/docker stop -t 10 %p +``` + +### stf-triproxy-dev.service + +This unit provides the `devside.stf.example.org` service mentioned earlier. Its purpose is to send and receive requests from the provider units, and distribute them across the processor units. It's "dumb" in that it contains no real logic, and you rarely if ever need to upgrade the unit. + +We call it a triproxy because it deals with three endpoints instead of the usual two. + +You may have more than one instance running simultaneously, and then give a comma separated list to the provider. For simplicity we're using a normal unit here. + +```ini +[Unit] +Description=STF dev triproxy +After=docker.service +Requires=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p +ExecStartPre=-/usr/bin/docker rm %p +ExecStart=/usr/bin/docker run --rm \ + --name %p \ + --net host \ + openstf/stf:1.0.0 \ + stf triproxy dev \ + --bind-pub "tcp://*:7250" \ + --bind-dealer "tcp://*:7260" \ + --bind-pull "tcp://*:7270" +ExecStop=-/usr/bin/docker stop -t 10 %p +``` + +### stf-websocket@.service + +**Requires** the `rethinkdb-proxy-28015.service` unit on the same host. + +The websocket unit provides the communication layer between client-side JavaScript and the server-side ZeroMQ+Protobuf combination. Almost every action in STF goes through the websocket unit. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-storage-plugin-image@3600.service` runs on port 3600). You can have multiple instances running on the same host by using different ports. + +```ini +[Unit] +Description=STF websocket +After=rethinkdb-proxy-28015.service +BindsTo=rethinkdb-proxy-28015.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + --link rethinkdb-proxy-28015:rethinkdb \ + -e "SECRET=YOUR_SESSION_SECRET_HERE" \ + -p 127.0.0.1:%i:3000 \ + openstf/stf:1.0.0 \ + stf websocket --port 3000 \ + --storage-url https://stf.example.org/ \ + --connect-sub tcp://appside.stf.example.org:7150 \ + --connect-push tcp://appside.stf.example.org:7170 +ExecStop=/usr/bin/docker stop -t 10 %p-%i +``` + +## Optional units + +These units are optional and don't affect the way STF works in any way. + +### stf-notify-hipchat.service + +If you use [HipChat](https://www.hipchat.com/), you can use this unit to push notifications to your room. Check `stf notify-hipchat --help` for more configuration options. + +Even if you don't use HipChat, you can use the code as a base for implementing a new notifier. + +Note that it doesn't make sense to have more than one instance of this unit running at once. You'd just get the same notifications twice. + +```ini +[Unit] +Description=STF HipChat notifier +After=docker.service +BindsTo=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:1.0.0 +ExecStartPre=-/usr/bin/docker kill %p +ExecStartPre=-/usr/bin/docker rm %p +ExecStart=/usr/bin/docker run --rm \ + --name %p \ + -e "HIPCHAT_TOKEN=YOUR_HIPCHAT_TOKEN_HERE" \ + -e "HIPCHAT_ROOM=YOUR_HIPCHAT_ROOM_HERE" \ + openstf/stf:1.0.0 \ + stf notify-hipchat \ + --connect-sub tcp://appside.stf.example.org:7150 +ExecStop=-/usr/bin/docker stop -t 10 %p +``` diff --git a/doc/topo-v1.ditaa b/doc/topo-v1.ditaa index bbbabd2d..79903f65 100644 --- a/doc/topo-v1.ditaa +++ b/doc/topo-v1.ditaa @@ -1,66 +1,67 @@ /------------\ /------------\ /------------\ | websocket | | websocket | | websocket | |------------| |------------| |------------| x N - | PUSH | SUB | | PUSH | SUB | | PUSH | SUB | - \------------/ \------------/ \------------/ - | ^ | ^ | ^ - | | | | | | - +-------------++--------------+ | - | | | | - +-------|------+--------------+ - | | - v | + | PUSH | SUB | | PUSH | SUB | | PUSH | SUB | /------------\ + \------------/ \------------/ \------------/ | notify | + | ^ | ^ | ^ |------------| + | | | | | | | SUB | + +-------------++--------------+ | \------------/ + | | | | ^ + +-------|------+--------------+ | + | | +------------------------------------+ + v | | /--------------\ - | PULL | PUB | - |--------------| -----------------| triproxy |----------------- x N - |--------------| - | DEALER | - \--------------/ - ^ - | - +----------------+----------------+ - | | | - v v v -/-------------\ /-------------\ /-------------\ -| DEALER | | DEALER | | DEALER | -|-------------| |-------------| |-------------| -| processor | | processor | | processor | x N -|-------------| |-------------| |-------------| -| DEALER | | DEALER | | DEALER | -\-------------/ \-------------/ \-------------/ - ^ ^ ^ - | | | - +----------------+----------------+ - | - v - /--------------\ - | DEALER | - |--------------| ------------------| triproxy |---------------- x N - |--------------| - +-------------->| PULL | PUB | - | \--------------/ - | ^ | - | | | - | +-------|------+----------------+ - | | | | | - | +-------------+-+---------------+ | - | | | | | | | - | | v | v | v - | /------------\ /------------\ /------------\ - | | PUSH | SUB | | PUSH | SUB | | PUSH | SUB | - | |------------| |------------| |------------| x N - | | dev | | dev | | dev | - | \------------/ \------------/ \------------/ - | ^ ^ ^ - | : : : - | +---------------+---+-----------+ - | : - | v - | /------------\ - +------------------| PUSH | | - |------| | - | | - | provider | - \------------/ + | PULL | PUB |----------------------------------+ + |--------------| | +----------------| triproxy |----------------- x N | + |--------------| | + | DEALER | | + \--------------/ | + ^ | + | | + +----------------+----------------+ | + | | | | + v v v v +/-------------\ /-------------\ /-------------\ /------------\ +| DEALER | | DEALER | | DEALER | | SUB | +|-------------| |-------------| |-------------| |------------| +| processor | | processor | | processor | x N | reaper | +|-------------| |-------------| |-------------| |------------| +| DEALER | | DEALER | | DEALER | | PUSH | +\-------------/ \-------------/ \-------------/ \------------/ + ^ ^ ^ | + | | | | + +----------------+----------------+ | + | | + v | + /--------------\ | + | DEALER | | + |--------------| | +-----------------| triproxy |---------------- x N | + |--------------| | + +-------------->| PULL | PUB | | + | \--------------/ | + | ^ ^ | | | + | | | | +----------------------------+ | + | | | | | | + | | +----|-------------------------------------+ + | +-------|------+----------------+ | + | | | | | | + | +-------------+-+---------------+ | | + | | | | | | | | + | | v | v | v | + | /------------\ /------------\ /------------\ | + | | PUSH | SUB | | PUSH | SUB | | PUSH | SUB | | + | |------------| |------------| |------------| x N | + | | dev | | dev | | dev | | + | \------------/ \------------/ \------------/ | + | ^ ^ ^ | + | : : : | + | +---------------+---------------+ | + | : | + | v | + | /------------\ | + +-----------------| PUSH | SUB |<-------------------------+ + |------------| + | provider | + \------------/ diff --git a/doc/topo-v1.png b/doc/topo-v1.png new file mode 100644 index 0000000000000000000000000000000000000000..2f6681dbca1b619b853db95110e646926ced539a GIT binary patch literal 32820 zcmce;2Rv5)-#<=KAC>f>6f)vSk`bcp?K%`0kricSkBn?-Te%Jqg)$RTX4x%;?2!>t z#AQS>%KX31CDrHt-uL~x$K(Eg9)0R^oa?;b=RIEI`Fy_K*N`u96#){8|&Mt&L{m3014 zd8+Zm;UhqJ{!m7wu*sunHP;O(EH)Nx8)clDnv#-|ay@sB`&vy+O;=Y} zcy``^dbwFC{`8(u<1Iht=e5U*iI+F;oL^WlE%PpT^5jWUQqseR;rIiqcT%7u{-tGQ zT%4Tu-FgUT&kjfN>g49-g#-tu)a#u%aY$4&Vy&}1Zzua-2%453O&wL)w0X0MiAiK+ zWNQ7X5|8YxtgQ8dD)7K(=f6?QVZ}^#cB8?;oA#TCYwPH=Wa{U9dVM23C1qf6&_Fb6!#@`#9tJ%Yq`r@diTO3I1ne~r01>$!oTMuzuaTl%CE`gTmSGpKjFVw?3vODi+|JIv<=G+)4Gn5)YKJi+ZEY;d-6mk3l)kjIp>BXU?Q(J)HCp*zA6pC&sg`&z7U@m{rgF z_pM{_`r_^m5>{_-!J2{XMELa1G=QIf>(;H>r`l{`(jypUWh=N;!{1!mKL6ms1ICcC z%$)N5lBDtRY$qr4zQfnaOBJIEM3!82O(OXS8OJM@XL45N0#@{`Rz@q9?dGRREosMe zMhSH%EWh@6_R&76dvP3+Wp1%(DKa`wPwu)!$_Fx_h)QU}b4u z2o^67f5=Wjy^t?ubEo{49JDP=eZ~**=4vmsov`m-sQSk+yz}+&@PMhD>+v)t5D0#L zWyWpN(~XMSwbM?2-ONpTFhFWa?_O>UE;bU-k3Oas5*ixiv3YZd-Mfb-rRTLA_$4Kc z_*oA--+k-YU3jcwpO{!-0nesQn=qPAMfZU6Fh89M_mkzmPmd%jk!=&YP7d=w;@Xrk z5;afEh!wGZVDQ3ep8>4h{4H!!=f}GWO#Nm~n3>(3zaR3z&y~cxGk`_Rez>Qk=oZfVfB)s3x_7e2Z`&JpvHO2N{nYSKgZS*PiRjtEBJLPv%_QUdWI6JMu%z2? z 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 F{{dy69n}B; literal 0 HcmV?d00001