A Dragonfly powered key-value store

As a way to explore how dragonfly creates sensors that we can interact with via the dripline protocol, let’s create and interact with a simple example: a key-value store which is implemented as a dripline endpoint. The key-value store will be very simple: a list of keys, each of which is a string (for example, key0, foo_key, some_name, whatever). Associated with each key is a floating point number which is its value: 1.3 perhaps, or -3.4, or 1.4e21, or really anything that can represent a floating point number. What we want as a user is some remote storage for such a key-value store, where we can ask dripline for the current value of key0 and have it answer with whatever that current value may be.

A quick refresher

First let’s recall how things are structured in dripline-python. The most basic functional unit in a dripline node is an endpoint. An endpoint represents something that we want to interact with - perhaps it is the contents of some file in a directory, or the input to a voltmeter, or in this case, a value associated with some key. In a dripline configuration file, an endpoint is always declared in a block whose name is endpoints.

Every endpoint must have a provider associated with it. The concept of a provider represents an element in the system without which an endpoint could not independently function. For example, consider a case where an endpoint represents the input to a digital voltmeter which is connected to an ethernet router. Without a connection to the voltmeter, we clearly can’t communicate with the endpoint. For that reason, we may write some code which performs the communication with the voltmeter itself, and then we say that code provides the endpoint which is the input to the voltmeter. It’s also the case that once we have one connection to the voltmeter, it seems like wasted effort to duplicate that among many objects, and so the voltmeter provider can act as a logical way to group endpoints together.

Let’s go

To start with, let’s consider the structure of our key-value store. We have a list of some keys that have string names, and each key has a floating point value associated with it. We will consider a list of items at a store, each of which has a price in dollars. It’s not a very big store - we only have peaches, chips, and waffles. The prices of these items fluctuates a lot due to global waffle demand, and so we want to be able to both ask our system what the current price is, and change the prices as necessary.

The dripline kv_store provider and kv_store_key endpoint will give us exactly this in a very simple way. If the current pricelist is something like

  • Peaches: 0.75
  • Chips: 1.75
  • Waffles: 4.00

We can write a configuration file for dragonfly (note, this is a reduced version of examples/kv_store_tutorial.yaml) that looks like this and represents our pricelist in a very recognizable way:

name: my_store
broker: localhost
module: Spimescape
endpoints:
  - name: my_price_list
    module: kv_store
    endpoints:
      - name: peaches
        module: kv_store_key
        initial_value: 0.75
      - name: chips
        module: kv_store_key
        initial_value: 1.75
      - name: waffles
        module: kv_store_key
        initial_value: 4.00

That’s it. Note in this modular structure that each level will have a name and module, and if it is a “Provider” it may have endpoints under it. Each name will correspond to a binding to the AMQP queue created. Throughout the config, the string value in the module field is referenced against the imported classes to find which object should be constructed. If the module takes arguments as enumerated in its initializer (__init__), those will be included at the same level (broker for Spimescape and initial_value for kv_store_key).

At the top level, the name parameter is simply telling dripline that we want our dripline node to be called my_store. The broker is telling dripline that there is an AMQP router which is installed on localhost. The module is telling dragonfly to create an instance of Spimescape, which is the most basic class for interacting with the dripline mesh. In the endpoints section, we declare the “endpoints” under my_store.

In the second level, we enumerate those endpoints, here only a single one with name of my_price_list. The module is kv_store, which instructs dragonfly to construct an object of type KVStore. The top-level Spimescape module is for generic interaction with the dripline mesh, whereas this KVStore contains any specific implementation. Again, as a provider, it has an endpoints section to declare the next level which will be the key-value pairs of interest.

At the bottom level, the name is the name of the item (the key) and the initial_value is the starting price (the value). The module again instructs dragonfly to construct objects of type KVStoreKey. It is instructive to note that there is no special relationship between the provider KVStore and endpoint KVStoreKey, one could add other endpoints from a different module.

Here we have an additional argument for each endpoint in initial_value. Dripline considers every parameter which isn’t called name or module to be specific to the object and passes it along to the object for it to do with as it likes. In this instance, the initializer of the KVStoreKey object looks like this:

def __init__(self, initial_value=None, **kwargs):
    self._value = initial_value

Note that initial_value is a keyword argument to the constructor, which sets whatever that parameter may be to be the initial value associated with the key.

Interacting with it

OK, enough details. To fire up our key-value store and start interacting with it, we want to start a dripline node which will use our configuration file. To do that, we will use dragonfly located in the bin directory with argument serve which invokes the open_spimescape_portal in the dragonfly/subcommands directory. We point it to our configuration file (if you are intrepid enough to make your own, point to whatever you created), and fire it up:

$ dragonfly serve -c examples/kv_store_tutorial.yaml -vvv

Notice the -vvv which sets the output to its most verbose. For each “v” you omit, one logging severity level will be omitted. If no -v option is given, normal operation (no warnings) should produce no terminal output. If you do the above, you should see output that looks like this:

warning: slack is only ever warning, setting that
2018-02-02T15:36:25[DEBUG   ] dragonfly(268) -> calling <dragonfly.subcommands.open_spimescape_portal.Serve object at 0x7f15250b88d0> with args:
{'name': 'my_store', 'keys': '#', 'broker': 'gus.p8', 'module': 'Spimescape', 'tmux': None, 'func': <dragonfly.subcommands.open_spimescape_portal.Serve object at 0x7f15250b88d0>, 'endpoints': [{'endpoints': [{'name': 'peaches', 'module': 'kv_store_key', 'initial_value': 0.75}, {'name': 'chips', 'module': 'kv_store_key', 'initial_value': 1.75}, {'name': 'waffles', 'module': 'kv_store_key', 'initial_value': 4.0}], 'name': 'my_price_list', 'module': 'kv_store'}], 'config': 'dragonfly/examples/kv_store_tutorial.yaml', 'verbose': 3}
2018-02-02T15:36:25[DEBUG   ] dripline.core.spimescape(47) -> cannot set keys, use self.endpoints
2018-02-02T15:36:25[INFO    ] dragonfly.subcommands.open_spimescape_portal(56) -> starting my_store
2018-02-02T15:36:25[INFO    ] dragonfly.subcommands.open_spimescape_portal(90) -> creating a <kv_store> with args:
{'name': 'my_price_list'}
2018-02-02T15:36:25[INFO    ] dragonfly.subcommands.open_spimescape_portal(90) -> creating a <kv_store_key> with args:
{'name': 'peaches', 'initial_value': 0.75}
2018-02-02T15:36:25[INFO    ] dragonfly.subcommands.open_spimescape_portal(90) -> creating a <kv_store_key> with args:
{'name': 'chips', 'initial_value': 1.75}
2018-02-02T15:36:25[INFO    ] dragonfly.subcommands.open_spimescape_portal(90) -> creating a <kv_store_key> with args:
{'name': 'waffles', 'initial_value': 4.0}
2018-02-02T15:36:25[INFO    ] dragonfly.subcommands.open_spimescape_portal(60) -> spimescapes created and populated
2018-02-02T15:36:25[INFO    ] dragonfly.subcommands.open_spimescape_portal(61) -> Configuration of my_store complete, starting consumption
2018-02-02T15:36:25[INFO    ] dripline.core.service(397) -> starting event loop for node my_store
-----------------------------
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(98) -> Connecting to localhost
2018-02-02T15:36:25[INFO    ] dripline.core.service(408) -> calling setup methods
2018-02-02T15:36:25[INFO    ] dripline.core.service(422) -> startup calls complete
-----------------------------
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(111) -> Connection opened
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(120) -> Adding connection close callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(163) -> Creating a new channel
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(175) -> Channel opened
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(187) -> Adding channel close callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(214) -> Declaring exchange requests
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(214) -> Declaring exchange alerts
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(226) -> Exchange declared
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(237) -> Declaring queue my_store
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(226) -> Exchange declared
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(237) -> Declaring queue my_store
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with broadcast.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with my_store.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with peaches.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with chips.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with waffles.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with my_price_list.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with broadcast.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with my_store.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with peaches.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with chips.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with waffles.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(255) -> Binding requests to my_store with my_price_list.#
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(267) -> Queue bound
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(280) -> Issuing consumer related RPC commands
2018-02-02T15:36:25[DEBUG   ] dripline.core.service(291) -> Adding consumer cancellation callback

There is high verbosity in the output, but not too much to follow. First dragonfly starts up and you can see that it will call open_spimescape_portal and the complete dictionary loaded from the config file. Then open_spimescape_portal creates in order each of the modules specified. At the first line break (—–) all modules have been constructed, the rest is AMQP magic.

First the connection to the AMQP broker is established. Then the relevant exchanges and queue are declared. Then all the bindings are established; everything in the config file with a name plus the global “broadcast” gets its own binding to the queue.

Now let’s start getting some prices. We’re going to use dragonfly to do this again, but with the the argument get, which invokes the dripline_agent in the dragonfly/subcommands directory. This is the common starting point for all interaction with dripline endpoints. First of all, let’s check the current price of peaches:

$ dragonfly get peaches
warning: slack is only ever warning, setting that
peaches(ret:0): [my_store]-> {u'value_raw': u'0.75'}

Nice. So the current price of peaches in our store is 0.75. We can also see that peaches is bound to the [my_store] queue (in case you forgot), and that it had a successful return code (ret:0). If you missed all that verbosity, you could again turn it on with a -vvv option, but that is mostly distracting.

What about waffles?

$ dragonfly get waffles
warning: slack is only ever warning, setting that
waffles(ret:0): [my_store]-> {u'value_raw': u'4.0'}

By default, dragonfly tries to connect to the broker on localhost. Dragonfly allows the broker to be specified with -b flag, so the above commands were identical to using a -b localhost option. If you have another computer on your local network (or any network that can see your amqp broker) then you can still query the endpoints by providing the broker address:

(on_amqp_server) $ dragonfly get peaches
warning: slack is only ever warning, setting that
peaches(ret:0): [my_store]-> {u'value_raw': u'0.75'}

(on_amqp_server) $ dragonfly get peaches -b localhost
warning: slack is only ever warning, setting that
peaches(ret:0): [my_store]-> {u'value_raw': u'0.75'}

(on_some_other_server) $ dragonfly get peaches -b <amqp.broker.server.address>
warning: slack is only ever warning, setting that
peaches(ret:0): [my_store]-> {u'value_raw': u'0.75'}

Now let’s say that there’s been a global rush on chips and the price we have to charge has skyrocketed from 1.75 to 1.79. We can use dragonfly again to set the new price, using the set argument. Again this invokes dripline_agent:

$ dragonfly get chips
warning: slack is only ever warning, setting that
chips(ret:0): [my_store]-> {u'value_raw': u'1.75'}

$ dragonfly set chips 1.79
warning: slack is only ever warning, setting that
chips->1.79(ret:0): [my_store]-> {u'values': [1.78]}

$ dragonfly get chips
warning: slack is only ever warning, setting that
chips(ret:0): [my_store]-> {u'value_raw': u'1.78'}

How did chips only get set to 1.78 when I explicitly asked for 1.79? A reasonable question, but one which quickly devolves into a discussion of floating point arithmetic in python 2. If you look in kv_store.py, you will discover that the on_set method attempts self._value = value - value % .01, i.e. it is rounding to the nearest cent. But the modulus in fractional division doesn’t make sense with floats, just open a python terminal:

>>> value = 1.79
>>> value - value % .01
1.78

And on that disappointing note, we close.