Daemon: basics
This example will establish a ‘metal’ store and items intended to portray market prices for various precious metals.
Getting started
The canonical approach to establish an mKTL daemon involves creating a Python module to represent all of your custom logic. Each of the steps described here is required, though there is more than one way to satisfy each step.
In order to successfully run the daemon the Python interpreter being used to
start the daemon must be able to import the mKTL module as well as any custom
module(s) implementing the subclass of Daemon
that encapsulates
the remainder of the functionality.
Daemon
subclass
The structure of the Python module containing the Daemon
subclass can
be completely arbitrary; for the sake of this example, the code is contained
in a metal
Python module, and the components described here are in a
precious
submodule.
Within the precious
submodule we define our subclass. For no reason other
than convenience it is defined with the name Daemon
. The structure of the
precious.py
file will be as follows:
import mktl
class Daemon(mktl.Daemon):
def setup(self):
pass
def setup_final(self):
pass
Item
subclasses
The items represent the interface the daemon presents to its clients; the
application-specific functionality that motivates the use of mKTL is exposed
via items, and for most applications this means one or more custom Item
subclasses.
An example subclass would have a structure like the following:
class MarketPriced(mktl.Item):
def __init__(self, *args, **kwargs):
mktl.Item.__init__(self, *args, **kwargs)
# Additional initialization steps would generally follow the
# regular initialization from the base class. In this case,
# our market-prices should update once per day:
self.poll(86400)
def req_refresh(self):
# Determine the current value for this item and return it.
pass
def req_set(self, request):
# Receive a request to set a new value for this item; return
# once the request is complete.
pass
Note in particular the documentation for Item.req_refresh()
and
Item.req_set()
, as it covers the expected behavior of each method.
For our example, the various items are intended to represent the market
spot price of different precious metals. In this case, the
Item.req_refresh()
method may request the current value from a website,
and Item.req_set()
would not be defined, since we don’t get to change
the actual market value. To pick one example:
class Gold(MarketPriced)
def req_refresh(self):
spot = get_spot_value('gold', 'usd', 'grams')
spot = float(spot)
payload = dict()
payload['value'] = spot
payload['time'] = time.time()
return spot
Daemon.setup()
method
Daemon.setup()
is the first pass of application-level setup for the
daemon. This is where instantiation of any custom subclasses of Item
needs to
occur, otherwise the various items defined for this daemon within this store
will be populated with default, caching-only instances; this occurs immediately
after the Daemon
invokes Daemon.setup()
.
Any application-specific initialization is reasonable to include in
Daemon.setup()
, such as initializing a connection to a controller,
especially if it is a pre-requisite for any of the custom Item
subclasses. For the example defined here, we only need to instantiate our
custom subclasses for each of the different precious metals for which we are
publishing prices:
def setup(self):
self.add_item(Gold, 'GOLD')
self.add_item(Silver, 'SILVER')
self.add_item(Platinum, 'PLATINUM')
Note the use of Daemon.add_item()
here when establishing authoritative
Item
instances. The Daemon.add_item()
method tweaks the
instantiation process such that the Item
is properly configured as
an authoritative instance, in addition to other tracking local to the
Daemon
instance that gets leveraged when handling requests.
Daemon.setup_final()
method
If this store contained any logic that must be executed only after all the items have been fully instantiated, and/or populated with any previously cached persistent values, that logic should be invoked in the:func:Daemon.setup_final method. Most daemons will not take advantage of this method; this example daemon is likewise too simple to require it.
A more complex example, such as a proxy for some other key/value protocol,
might establish all of the Item
instances, and the in the
Daemon.setup_final()
method, it would take whatever actions are necessary
in the foreign protocol to subscribe to event broadcasts.
Setting values of other items
This scenario is not relevant to this simple example, but it occurs often
in actual usage. What if you have code that needs to set the value of an
Item
instance within the context of a single daemon? Not every
item will have a custom subclass defined for it; the default behavior of
a ‘caching’ item is adequate for a large fraction of authoritative items.
It’s common for there to be an item that sets derived values elsewhere,
or sets a family of related item values as a result of an isolated change
elsewhere.
The Item.get()
and Item.set()
methods are inherently client-facing.
While they can be used in a daemon context they will invoke the full mKTL
request handling; in some cases this will be desired, but in the average
case it is not necessary, or desired– and adds extra overhead.
Updating the value for an authoritative Item
is done via the
Item.publish()
method. The Item.value
property, for
authoritative items, will map to this method. These two calls are
equivalent, only one is necessary:
self.value = 102.45
self.publish(102.45)
Likewise, for other authoritative items within a daemon, with two equivalent ways to retrieve the local authoritative item instance:
other = self.store['OTHER_ITEM']
other = mktl.get('metal.OTHER_ITEM')
other.value = 33.67
other.publish(33.67)
JSON description of items
The configuration syntax describing a set of items is a JSON associative array. When a daemon first starts it must have a complete JSON description of every item; this forms the core of the configuration managed by that daemon, which is responsible for adding the metadata required for proper client interactions.
The JSON contents can be generated at run time and ‘saved’ for future use by other mKTL calls. This is the approach taken by the KTL protocol translation, where the JSON is a repackaging of the configuration metadata supplied by KTL API calls; with a JSON-like dictionary in hand, the daemon would have lines like the following in its initialization method:
def __init__(self, *args, **kwargs):
items = generate_config()
mktl.Config.File.save_daemon('metal', 'precious', items)
mktl.Daemon.__init__(self, *args, **kwargs)
It’s more likely that the JSON configuration is written out as a file, ready to be used by the daemon. The file can be anywhere, so long as it is accessible upon startup, after which the file is no longer referenced in any way. The configuration file is expected to contain a single JSON-formatted dictionary, with a dictionary for each item. Whitespace is not important, so long as JSON parsers understand the file contents.
The following is a configuration block appropriate for the items used in this example:
{
"GOLD": {
"type": "numeric",
"units": {
"base": "USD/gram"
"formatted": "USD/gram",
},
"description": "Current spot price for pure gold.",
"settable": false
},
"PLATINUM": {
"type": "numeric",
"units": {
"base": "USD/gram"
"formatted": "USD/gram",
},
"description": "Current spot price for pure platinum.",
"settable": false
},
"SILVER": {
"type": "numeric",
"units": {
"base": "USD/gram"
"formatted": "USD/gram",
},
"description": "Current spot price for pure silver.",
"settable": false
}
}
Starting the daemon
The markguided executable is a persistent application that enables clients to easily find authoritative mKTL daemons. Having one instance of markguided running on the local network is recommended.
The markd executable provides a common entry point for a persistent daemon. Assuming the default search path is set up correctly, for the example outlined here the invocation would resemble:
markd metal precious --module metal.precious -c precious_metals.json
Both markguided and markd should be running before mKTL client interactions are attempted.