Stratus is working really well. Phase II: use it for IPC (pub/sub store-n-forward)

Findings:

  • HTTPS continues to be nontrivial
    • need to push on this a little bit harder just to be sure
    • no hard requirement tho, so maybe screw it?
  • client-side caching is super effective; ~ 1kb cache stores 10 of everything (incl Strings), ~ 10% performance cost vs. uncached access to int32
  • Shared per-app secret is OK. Functionally similar to API key / API secret. Write requests are signed with the shared secret, API key is essentially the per-device GUID. Requires no per-device configurations in firmware.
  • Not attempted: a "configurator" program which wold include all GUIDs + per-device private keys, burn the key into EEPROM. Not clear whether this is required or even meaningful...
  • Timing doesn't (cannot) matter. Specifically, race conditions would make fine-grained sequence-of-publication difficult to enforce. Therefore it probably doesn't (shouldn't) matter in which order subscriptions are replayed

Concerns:

  • writes might be pretty slow, should look into caching / batching
    • publish() queue, flush with update()
  • no real threading available for asynchronous updates
  • pub/sub pattern is attractive, but (because threading) the sub is in the foreground during update(). pub might be queued or synchronous.

Core functionality / API:

  • subscribe(stream, callback, scope=PRIVATE, limit=none, reset=false)
    • for each event: callback(stream, data)
    • if specified, will re-send events up to limit events (oldest -> newest)
    • should be able to reset his access counter
    • should also be able to unsubscribe
      • a client-side activity; server doesn't care
  • publish(stream, data, scope=PRIVATE, ttl=SHORTISH, queue=false)
    • publishes String data to stream
      • this may trigger a subscribed callback, synchronously
    • if specified, can queue data for the next update()
  • update()
    • updates the configuration (accessed via get())
    • sends any queued data
    • downloads any subscription data
      • may trigger callbacks, synchronously
  • maybeUpdate(interval=get("REFRESH INTERVAL"))
    • helper function, trivial
    • if interval seconds have passed, update()

Scope is just a channel, identified with a String. For simplicity any Stratus node can subscribe to any channels, but one per subscribe() call (assuming acl permits such)

Publishing to a PRIVATE (unreadable) scope with a very long TTL is just a key/value store.

HTTP/1.1 & keepAlive (persistent TCP) would improve performance measurably. Does that matter?

  • annoying to implement (TCP client & socket server)
  • Do we care if the MCU pauses for a ~ second during an update?
  • Probably decide when HTTPClient stops working out (e.g. very long timeouts or something)

IOT MQ table would require:

  • GUID of the writer (ownership)
  • Scope (PRIVATE or other keyword)
  • key & value (both String, which can be interpreted later to whatever)
  • TTL
  • timestamp

Access control? Stratus MQ has a name (URL) and a secret, access would also require a GUID. Do we need to further grant access by GUID (and therefore maintain a table of queue, guid, secret, approve/deny) ? -> yes, this allows for publish-only things (data sources) or subscribe-only (consumers). Implementation is (was) straightforward

Implementation details:

  • publish: action=publish, fields: key, value, ttl, scope, IOTMQ, GUID, signature
  • subscribe: action=subscribe, fields: key, scope, IOTMQ, GUID, signature

Access records (approve & reject) can be stored in the queue (private scope to the server), with a longish TTL. Rejects would be consumed for logging purposes or for an admin console ("approve this access"). Approved access can be consumed to track subscription (in which case the TTL can be pretty short).

Do we need a TTL based on message depth? "store N messages for at most M seconds" No current use case.

Do we need default TTLs for queues (either #messages, or time-per-message) ? #messages over time? no use case.